From 552a772cc18c10a4926f2fd543f17e038bd0fc98 Mon Sep 17 00:00:00 2001 From: keorn Date: Thu, 22 Dec 2016 07:06:40 +0100 Subject: [PATCH 01/22] make fields defaulting to 0 optional --- ethcore/src/spec/genesis.rs | 8 ++++---- ethcore/src/spec/spec.rs | 2 +- json/src/blockchain/blockchain.rs | 6 +++--- json/src/spec/genesis.rs | 12 ++++++------ json/src/spec/params.rs | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ethcore/src/spec/genesis.rs b/ethcore/src/spec/genesis.rs index be3b7c808..1fad0836d 100644 --- a/ethcore/src/spec/genesis.rs +++ b/ethcore/src/spec/genesis.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -use util::{Address, H256, Uint, U256}; +use util::{Address, H256, Uint, U256, FixedHash}; use util::sha3::SHA3_NULL_RLP; use ethjson; use super::seal::Seal; @@ -50,9 +50,9 @@ impl From for Genesis { Genesis { seal: From::from(g.seal), difficulty: g.difficulty.into(), - author: g.author.into(), - timestamp: g.timestamp.into(), - parent_hash: g.parent_hash.into(), + author: g.author.map_or_else(Address::zero, Into::into), + timestamp: g.timestamp.map_or(0, Into::into), + parent_hash: g.parent_hash.map_or_else(H256::zero, Into::into), gas_limit: g.gas_limit.into(), transactions_root: g.transactions_root.map_or_else(|| SHA3_NULL_RLP.clone(), Into::into), receipts_root: g.receipts_root.map_or_else(|| SHA3_NULL_RLP.clone(), Into::into), diff --git a/ethcore/src/spec/spec.rs b/ethcore/src/spec/spec.rs index bdcd5eee2..b6a688402 100644 --- a/ethcore/src/spec/spec.rs +++ b/ethcore/src/spec/spec.rs @@ -49,7 +49,7 @@ pub struct CommonParams { impl From for CommonParams { fn from(p: ethjson::spec::Params) -> Self { CommonParams { - account_start_nonce: p.account_start_nonce.into(), + account_start_nonce: p.account_start_nonce.map_or_else(U256::zero, Into::into), maximum_extra_data_size: p.maximum_extra_data_size.into(), network_id: p.network_id.into(), subprotocol_name: p.subprotocol_name.unwrap_or_else(|| "eth".to_owned()), diff --git a/json/src/blockchain/blockchain.rs b/json/src/blockchain/blockchain.rs index 8a0de8801..9d18796da 100644 --- a/json/src/blockchain/blockchain.rs +++ b/json/src/blockchain/blockchain.rs @@ -59,9 +59,9 @@ impl BlockChain { mix_hash: self.genesis_block.mix_hash.clone(), }), difficulty: self.genesis_block.difficulty, - author: self.genesis_block.author.clone(), - timestamp: self.genesis_block.timestamp, - parent_hash: self.genesis_block.parent_hash.clone(), + author: Some(self.genesis_block.author.clone()), + timestamp: Some(self.genesis_block.timestamp), + parent_hash: Some(self.genesis_block.parent_hash.clone()), gas_limit: self.genesis_block.gas_limit, transactions_root: Some(self.genesis_block.transactions_root.clone()), receipts_root: Some(self.genesis_block.receipts_root.clone()), diff --git a/json/src/spec/genesis.rs b/json/src/spec/genesis.rs index c732a1293..393bc49d5 100644 --- a/json/src/spec/genesis.rs +++ b/json/src/spec/genesis.rs @@ -28,13 +28,13 @@ pub struct Genesis { pub seal: Seal, /// Difficulty. pub difficulty: Uint, - /// Block author. - pub author: Address, - /// Block timestamp. - pub timestamp: Uint, - /// Parent hash. + /// Block author, defaults to 0. + pub author: Option
, + /// Block timestamp, defaults to 0. + pub timestamp: Option, + /// Parent hash, defaults to 0. #[serde(rename="parentHash")] - pub parent_hash: H256, + pub parent_hash: Option, /// Gas limit. #[serde(rename="gasLimit")] pub gas_limit: Uint, diff --git a/json/src/spec/params.rs b/json/src/spec/params.rs index 882686319..f4492f874 100644 --- a/json/src/spec/params.rs +++ b/json/src/spec/params.rs @@ -22,9 +22,9 @@ use hash::H256; /// Spec params. #[derive(Debug, PartialEq, Deserialize)] pub struct Params { - /// Account start nonce. + /// Account start nonce, defaults to 0. #[serde(rename="accountStartNonce")] - pub account_start_nonce: Uint, + pub account_start_nonce: Option, /// Maximum size of extra data. #[serde(rename="maximumExtraDataSize")] pub maximum_extra_data_size: Uint, From 7c715aeec3449b61d32479898a878613024f3f44 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Tue, 3 Jan 2017 16:32:50 +0100 Subject: [PATCH 02/22] basic account type --- ethcore/src/state/account.rs | 67 +++++++++++++++++++++++++++++------- ethcore/src/state/mod.rs | 2 +- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/ethcore/src/state/account.rs b/ethcore/src/state/account.rs index 49cebd550..9d6b58d0e 100644 --- a/ethcore/src/state/account.rs +++ b/ethcore/src/state/account.rs @@ -25,6 +25,41 @@ use std::cell::{RefCell, Cell}; const STORAGE_CACHE_ITEMS: usize = 8192; +/// Basic account type. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BasicAccount { + /// Nonce of the account. + pub nonce: U256, + /// Balance of the account. + pub balance: U256, + /// Storage root of the account. + pub storage_root: H256, + /// Code hash of the account. + pub code_hash: H256, +} + +impl Encodable for BasicAccount { + fn rlp_append(&self, s: &mut RlpStream) { + s.begin_list(4) + .append(&self.nonce) + .append(&self.balance) + .append(&self.storage_root) + .append(&self.code_hash); + } +} + +impl Decodable for BasicAccount { + fn decode(decoder: &D) -> Result where D: Decoder { + let rlp = decoder.as_rlp(); + Ok(BasicAccount { + nonce: rlp.val_at(0)?, + balance: rlp.val_at(1)?, + storage_root: rlp.val_at(2)?, + code_hash: rlp.val_at(3)?, + }) + } +} + /// Single account in the system. /// Keeps track of changes to the code and storage. /// The changes are applied in `commit_storage` and `commit_code` @@ -53,6 +88,23 @@ pub struct Account { address_hash: Cell>, } +impl From for Account { + fn from(basic: BasicAccount) -> Self { + Account { + balance: basic.balance, + nonce: basic.nonce, + storage_root: basic.storage_root, + storage_cache: Self::empty_storage_cache(), + storage_changes: HashMap::new(), + code_hash: basic.code_hash, + code_size: None, + code_cache: Arc::new(vec![]), + code_filth: Filth::Clean, + address_hash: Cell::new(None), + } + } +} + impl Account { #[cfg(test)] /// General constructor. @@ -109,19 +161,8 @@ impl Account { /// Create a new account from RLP. pub fn from_rlp(rlp: &[u8]) -> Account { - let r: Rlp = Rlp::new(rlp); - Account { - nonce: r.val_at(0), - balance: r.val_at(1), - storage_root: r.val_at(2), - storage_cache: Self::empty_storage_cache(), - storage_changes: HashMap::new(), - code_hash: r.val_at(3), - code_cache: Arc::new(vec![]), - code_size: None, - code_filth: Filth::Clean, - address_hash: Cell::new(None), - } + let basic: BasicAccount = ::rlp::decode(rlp); + basic.into() } /// Create a new contract account. diff --git a/ethcore/src/state/mod.rs b/ethcore/src/state/mod.rs index c9730c1c3..1dcc37732 100644 --- a/ethcore/src/state/mod.rs +++ b/ethcore/src/state/mod.rs @@ -37,7 +37,7 @@ use util::trie::recorder::{Recorder, BasicRecorder as TrieRecorder}; mod account; mod substate; -pub use self::account::Account; +pub use self::account::{BasicAccount, Account}; pub use self::substate::Substate; /// Used to return information about an `State::apply` operation. From eb2b1ad5daf93453e82092761a7c60da4ae6a465 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Tue, 3 Jan 2017 16:43:22 +0100 Subject: [PATCH 03/22] move basic_account to types module --- ethcore/src/state/account.rs | 36 +------------------ ethcore/src/state/mod.rs | 2 +- ethcore/src/types/basic_account.rs | 55 ++++++++++++++++++++++++++++++ ethcore/src/types/mod.rs.in | 1 + 4 files changed, 58 insertions(+), 36 deletions(-) create mode 100644 ethcore/src/types/basic_account.rs diff --git a/ethcore/src/state/account.rs b/ethcore/src/state/account.rs index 9d6b58d0e..63e8ff9de 100644 --- a/ethcore/src/state/account.rs +++ b/ethcore/src/state/account.rs @@ -20,46 +20,12 @@ use util::*; use pod_account::*; use rlp::*; use lru_cache::LruCache; +use basic_account::BasicAccount; use std::cell::{RefCell, Cell}; const STORAGE_CACHE_ITEMS: usize = 8192; -/// Basic account type. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct BasicAccount { - /// Nonce of the account. - pub nonce: U256, - /// Balance of the account. - pub balance: U256, - /// Storage root of the account. - pub storage_root: H256, - /// Code hash of the account. - pub code_hash: H256, -} - -impl Encodable for BasicAccount { - fn rlp_append(&self, s: &mut RlpStream) { - s.begin_list(4) - .append(&self.nonce) - .append(&self.balance) - .append(&self.storage_root) - .append(&self.code_hash); - } -} - -impl Decodable for BasicAccount { - fn decode(decoder: &D) -> Result where D: Decoder { - let rlp = decoder.as_rlp(); - Ok(BasicAccount { - nonce: rlp.val_at(0)?, - balance: rlp.val_at(1)?, - storage_root: rlp.val_at(2)?, - code_hash: rlp.val_at(3)?, - }) - } -} - /// Single account in the system. /// Keeps track of changes to the code and storage. /// The changes are applied in `commit_storage` and `commit_code` diff --git a/ethcore/src/state/mod.rs b/ethcore/src/state/mod.rs index 1dcc37732..c9730c1c3 100644 --- a/ethcore/src/state/mod.rs +++ b/ethcore/src/state/mod.rs @@ -37,7 +37,7 @@ use util::trie::recorder::{Recorder, BasicRecorder as TrieRecorder}; mod account; mod substate; -pub use self::account::{BasicAccount, Account}; +pub use self::account::Account; pub use self::substate::Substate; /// Used to return information about an `State::apply` operation. diff --git a/ethcore/src/types/basic_account.rs b/ethcore/src/types/basic_account.rs new file mode 100644 index 000000000..18adbc944 --- /dev/null +++ b/ethcore/src/types/basic_account.rs @@ -0,0 +1,55 @@ +// Copyright 2015, 2016 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 . + +//! Basic account type -- the decoded RLP from the state trie. + +use rlp::*; +use util::{U256, H256}; + +/// Basic account type. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BasicAccount { + /// Nonce of the account. + pub nonce: U256, + /// Balance of the account. + pub balance: U256, + /// Storage root of the account. + pub storage_root: H256, + /// Code hash of the account. + pub code_hash: H256, +} + +impl Encodable for BasicAccount { + fn rlp_append(&self, s: &mut RlpStream) { + s.begin_list(4) + .append(&self.nonce) + .append(&self.balance) + .append(&self.storage_root) + .append(&self.code_hash); + } +} + +impl Decodable for BasicAccount { + fn decode(decoder: &D) -> Result where D: Decoder { + let rlp = decoder.as_rlp(); + Ok(BasicAccount { + nonce: rlp.val_at(0)?, + balance: rlp.val_at(1)?, + storage_root: rlp.val_at(2)?, + code_hash: rlp.val_at(3)?, + }) + } +} diff --git a/ethcore/src/types/mod.rs.in b/ethcore/src/types/mod.rs.in index 567c6fff9..12081d1fb 100644 --- a/ethcore/src/types/mod.rs.in +++ b/ethcore/src/types/mod.rs.in @@ -37,3 +37,4 @@ pub mod mode; pub mod pruning_info; pub mod security_level; pub mod encoded; +pub mod basic_account; From 5d2cf22ef46bd376a171d0ee08417042e80e56d7 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Tue, 3 Jan 2017 17:05:27 +0100 Subject: [PATCH 04/22] use basic_account in snapshot --- ethcore/src/snapshot/account.rs | 293 +++++++++++--------------- ethcore/src/snapshot/mod.rs | 11 +- ethcore/src/snapshot/tests/helpers.rs | 8 +- ethcore/src/snapshot/tests/state.rs | 16 +- 4 files changed, 142 insertions(+), 186 deletions(-) diff --git a/ethcore/src/snapshot/account.rs b/ethcore/src/snapshot/account.rs index 0dfb72955..9d9aa0b9e 100644 --- a/ethcore/src/snapshot/account.rs +++ b/ethcore/src/snapshot/account.rs @@ -17,16 +17,17 @@ //! Account state encoding and decoding use account_db::{AccountDB, AccountDBMut}; +use basic_account::BasicAccount; use snapshot::Error; use util::{U256, FixedHash, H256, Bytes, HashDB, SHA3_EMPTY, SHA3_NULL_RLP}; use util::trie::{TrieDB, Trie}; -use rlp::{Rlp, RlpStream, Stream, UntrustedRlp, View}; +use rlp::{RlpStream, Stream, UntrustedRlp, View}; use std::collections::HashSet; // An empty account -- these are replaced with RLP null data for a space optimization. -const ACC_EMPTY: Account = Account { +const ACC_EMPTY: BasicAccount = BasicAccount { nonce: U256([0, 0, 0, 0]), balance: U256([0, 0, 0, 0]), storage_root: SHA3_NULL_RLP, @@ -59,165 +60,121 @@ impl CodeState { } } -// An alternate account structure from ::account::Account. -#[derive(PartialEq, Clone, Debug)] -pub struct Account { - nonce: U256, - balance: U256, - storage_root: H256, - code_hash: H256, +// walk the account's storage trie, returning an RLP item containing the +// account properties and the storage. +pub fn to_fat_rlp(acc: &BasicAccount, acct_db: &AccountDB, used_code: &mut HashSet) -> Result { + if acc == &ACC_EMPTY { + return Ok(::rlp::NULL_RLP.to_vec()); + } + + let db = TrieDB::new(acct_db, &acc.storage_root)?; + + let mut pairs = Vec::new(); + + for item in db.iter()? { + let (k, v) = item?; + pairs.push((k, v)); + } + + let mut stream = RlpStream::new_list(pairs.len()); + + for (k, v) in pairs { + stream.begin_list(2).append(&k).append(&&*v); + } + + let pairs_rlp = stream.out(); + + let mut account_stream = RlpStream::new_list(5); + account_stream.append(&acc.nonce) + .append(&acc.balance); + + // [has_code, code_hash]. + if acc.code_hash == SHA3_EMPTY { + account_stream.append(&CodeState::Empty.raw()).append_empty_data(); + } else if used_code.contains(&acc.code_hash) { + account_stream.append(&CodeState::Hash.raw()).append(&acc.code_hash); + } else { + match acct_db.get(&acc.code_hash) { + Some(c) => { + used_code.insert(acc.code_hash.clone()); + account_stream.append(&CodeState::Inline.raw()).append(&&*c); + } + None => { + warn!("code lookup failed during snapshot"); + account_stream.append(&false).append_empty_data(); + } + } + } + + account_stream.append_raw(&pairs_rlp, 1); + + Ok(account_stream.out()) } -impl Account { - // decode the account from rlp. - pub fn from_thin_rlp(rlp: &[u8]) -> Self { - let r: Rlp = Rlp::new(rlp); +// decode a fat rlp, and rebuild the storage trie as we go. +// returns the account structure along with its newly recovered code, +// if it exists. +pub fn from_fat_rlp( + acct_db: &mut AccountDBMut, + rlp: UntrustedRlp, +) -> Result<(BasicAccount, Option), Error> { + use util::{TrieDBMut, TrieMut}; - Account { - nonce: r.val_at(0), - balance: r.val_at(1), - storage_root: r.val_at(2), - code_hash: r.val_at(3), + // check for special case of empty account. + if rlp.is_empty() { + return Ok((ACC_EMPTY, None)); + } + + let nonce = rlp.val_at(0)?; + let balance = rlp.val_at(1)?; + let code_state: CodeState = { + let raw: u8 = rlp.val_at(2)?; + CodeState::from(raw)? + }; + + // load the code if it exists. + let (code_hash, new_code) = match code_state { + CodeState::Empty => (SHA3_EMPTY, None), + CodeState::Inline => { + let code: Bytes = rlp.val_at(3)?; + let code_hash = acct_db.insert(&code); + + (code_hash, Some(code)) + } + CodeState::Hash => { + let code_hash = rlp.val_at(3)?; + + (code_hash, None) + } + }; + + let mut storage_root = H256::zero(); + + { + let mut storage_trie = TrieDBMut::new(acct_db, &mut storage_root); + let pairs = rlp.at(4)?; + for pair_rlp in pairs.iter() { + let k: Bytes = pair_rlp.val_at(0)?; + let v: Bytes = pair_rlp.val_at(1)?; + + storage_trie.insert(&k, &v)?; } } - // encode the account to a standard rlp. - pub fn to_thin_rlp(&self) -> Bytes { - let mut stream = RlpStream::new_list(4); - stream - .append(&self.nonce) - .append(&self.balance) - .append(&self.storage_root) - .append(&self.code_hash); + let acc = BasicAccount { + nonce: nonce, + balance: balance, + storage_root: storage_root, + code_hash: code_hash, + }; - stream.out() - } - - // walk the account's storage trie, returning an RLP item containing the - // account properties and the storage. - pub fn to_fat_rlp(&self, acct_db: &AccountDB, used_code: &mut HashSet) -> Result { - if self == &ACC_EMPTY { - return Ok(::rlp::NULL_RLP.to_vec()); - } - - let db = TrieDB::new(acct_db, &self.storage_root)?; - - let mut pairs = Vec::new(); - - for item in db.iter()? { - let (k, v) = item?; - pairs.push((k, v)); - } - - let mut stream = RlpStream::new_list(pairs.len()); - - for (k, v) in pairs { - stream.begin_list(2).append(&k).append(&&*v); - } - - let pairs_rlp = stream.out(); - - let mut account_stream = RlpStream::new_list(5); - account_stream.append(&self.nonce) - .append(&self.balance); - - // [has_code, code_hash]. - if self.code_hash == SHA3_EMPTY { - account_stream.append(&CodeState::Empty.raw()).append_empty_data(); - } else if used_code.contains(&self.code_hash) { - account_stream.append(&CodeState::Hash.raw()).append(&self.code_hash); - } else { - match acct_db.get(&self.code_hash) { - Some(c) => { - used_code.insert(self.code_hash.clone()); - account_stream.append(&CodeState::Inline.raw()).append(&&*c); - } - None => { - warn!("code lookup failed during snapshot"); - account_stream.append(&false).append_empty_data(); - } - } - } - - account_stream.append_raw(&pairs_rlp, 1); - - Ok(account_stream.out()) - } - - // decode a fat rlp, and rebuild the storage trie as we go. - // returns the account structure along with its newly recovered code, - // if it exists. - pub fn from_fat_rlp( - acct_db: &mut AccountDBMut, - rlp: UntrustedRlp, - ) -> Result<(Self, Option), Error> { - use util::{TrieDBMut, TrieMut}; - - // check for special case of empty account. - if rlp.is_empty() { - return Ok((ACC_EMPTY, None)); - } - - let nonce = rlp.val_at(0)?; - let balance = rlp.val_at(1)?; - let code_state: CodeState = { - let raw: u8 = rlp.val_at(2)?; - CodeState::from(raw)? - }; - - // load the code if it exists. - let (code_hash, new_code) = match code_state { - CodeState::Empty => (SHA3_EMPTY, None), - CodeState::Inline => { - let code: Bytes = rlp.val_at(3)?; - let code_hash = acct_db.insert(&code); - - (code_hash, Some(code)) - } - CodeState::Hash => { - let code_hash = rlp.val_at(3)?; - - (code_hash, None) - } - }; - - let mut storage_root = H256::zero(); - - { - let mut storage_trie = TrieDBMut::new(acct_db, &mut storage_root); - let pairs = rlp.at(4)?; - for pair_rlp in pairs.iter() { - let k: Bytes = pair_rlp.val_at(0)?; - let v: Bytes = pair_rlp.val_at(1)?; - - storage_trie.insert(&k, &v)?; - } - } - - let acc = Account { - nonce: nonce, - balance: balance, - storage_root: storage_root, - code_hash: code_hash, - }; - - Ok((acc, new_code)) - } - - /// Get the account's code hash. - pub fn code_hash(&self) -> &H256 { - &self.code_hash - } - - #[cfg(test)] - pub fn storage_root_mut(&mut self) -> &mut H256 { - &mut self.storage_root - } + Ok((acc, new_code)) } #[cfg(test)] mod tests { use account_db::{AccountDB, AccountDBMut}; + use basic_account::BasicAccount; use tests::helpers::get_temp_state_db; use snapshot::tests::helpers::fill_storage; @@ -227,26 +184,26 @@ mod tests { use std::collections::HashSet; - use super::{ACC_EMPTY, Account}; + use super::{ACC_EMPTY, to_fat_rlp, from_fat_rlp}; #[test] fn encoding_basic() { let mut db = get_temp_state_db(); let addr = Address::random(); - let account = Account { + let account = BasicAccount { nonce: 50.into(), balance: 123456789.into(), storage_root: SHA3_NULL_RLP, code_hash: SHA3_EMPTY, }; - let thin_rlp = account.to_thin_rlp(); - assert_eq!(Account::from_thin_rlp(&thin_rlp), account); + let thin_rlp = ::rlp::encode(&account); + assert_eq!(::rlp::decode::(&thin_rlp), account); - let fat_rlp = account.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &addr), &mut Default::default()).unwrap(); + let fat_rlp = to_fat_rlp(&account, &AccountDB::new(db.as_hashdb(), &addr), &mut Default::default()).unwrap(); let fat_rlp = UntrustedRlp::new(&fat_rlp); - assert_eq!(Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr), fat_rlp).unwrap().0, account); + assert_eq!(from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr), fat_rlp).unwrap().0, account); } #[test] @@ -258,7 +215,7 @@ mod tests { let acct_db = AccountDBMut::new(db.as_hashdb_mut(), &addr); let mut root = SHA3_NULL_RLP; fill_storage(acct_db, &mut root, &mut H256::zero()); - Account { + BasicAccount { nonce: 25.into(), balance: 987654321.into(), storage_root: root, @@ -266,12 +223,12 @@ mod tests { } }; - let thin_rlp = account.to_thin_rlp(); - assert_eq!(Account::from_thin_rlp(&thin_rlp), account); + let thin_rlp = ::rlp::encode(&account); + assert_eq!(::rlp::decode::(&thin_rlp), account); - let fat_rlp = account.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &addr), &mut Default::default()).unwrap(); + let fat_rlp = to_fat_rlp(&account, &AccountDB::new(db.as_hashdb(), &addr), &mut Default::default()).unwrap(); let fat_rlp = UntrustedRlp::new(&fat_rlp); - assert_eq!(Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr), fat_rlp).unwrap().0, account); + assert_eq!(from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr), fat_rlp).unwrap().0, account); } #[test] @@ -291,14 +248,14 @@ mod tests { acct_db.emplace(code_hash.clone(), DBValue::from_slice(b"this is definitely code")); } - let account1 = Account { + let account1 = BasicAccount { nonce: 50.into(), balance: 123456789.into(), storage_root: SHA3_NULL_RLP, code_hash: code_hash, }; - let account2 = Account { + let account2 = BasicAccount { nonce: 400.into(), balance: 98765432123456789usize.into(), storage_root: SHA3_NULL_RLP, @@ -307,18 +264,18 @@ mod tests { let mut used_code = HashSet::new(); - let fat_rlp1 = account1.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &addr1), &mut used_code).unwrap(); - let fat_rlp2 = account2.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &addr2), &mut used_code).unwrap(); + let fat_rlp1 = to_fat_rlp(&account1, &AccountDB::new(db.as_hashdb(), &addr1), &mut used_code).unwrap(); + let fat_rlp2 = to_fat_rlp(&account2, &AccountDB::new(db.as_hashdb(), &addr2), &mut used_code).unwrap(); assert_eq!(used_code.len(), 1); let fat_rlp1 = UntrustedRlp::new(&fat_rlp1); let fat_rlp2 = UntrustedRlp::new(&fat_rlp2); - let (acc, maybe_code) = Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr2), fat_rlp2).unwrap(); + let (acc, maybe_code) = from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr2), fat_rlp2).unwrap(); assert!(maybe_code.is_none()); assert_eq!(acc, account2); - let (acc, maybe_code) = Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr1), fat_rlp1).unwrap(); + let (acc, maybe_code) = from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr1), fat_rlp1).unwrap(); assert_eq!(maybe_code, Some(b"this is definitely code".to_vec())); assert_eq!(acc, account1); } @@ -328,7 +285,7 @@ mod tests { let mut db = get_temp_state_db(); let mut used_code = HashSet::new(); - assert_eq!(ACC_EMPTY.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &Address::default()), &mut used_code).unwrap(), ::rlp::NULL_RLP.to_vec()); - assert_eq!(Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &Address::default()), UntrustedRlp::new(&::rlp::NULL_RLP)).unwrap(), (ACC_EMPTY, None)); + assert_eq!(to_fat_rlp(&ACC_EMPTY, &AccountDB::new(db.as_hashdb(), &Address::default()), &mut used_code).unwrap(), ::rlp::NULL_RLP.to_vec()); + assert_eq!(from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &Address::default()), UntrustedRlp::new(&::rlp::NULL_RLP)).unwrap(), (ACC_EMPTY, None)); } } diff --git a/ethcore/src/snapshot/mod.rs b/ethcore/src/snapshot/mod.rs index 56fd09200..add5029b2 100644 --- a/ethcore/src/snapshot/mod.rs +++ b/ethcore/src/snapshot/mod.rs @@ -40,7 +40,6 @@ use util::sha3::SHA3_NULL_RLP; use rlp::{RlpStream, Stream, UntrustedRlp, View}; use bloom_journal::Bloom; -use self::account::Account; use self::block::AbridgedBlock; use self::io::SnapshotWriter; @@ -368,12 +367,12 @@ pub fn chunk_state<'a>(db: &HashDB, root: &H256, writer: &Mutex status.new_code.push((code_hash, code, hash)), @@ -534,7 +533,7 @@ fn rebuild_accounts( } } - acc.to_thin_rlp() + ::rlp::encode(&acc).to_vec() }; *out = (hash, thin_rlp); diff --git a/ethcore/src/snapshot/tests/helpers.rs b/ethcore/src/snapshot/tests/helpers.rs index 164f99121..8d7d1bb97 100644 --- a/ethcore/src/snapshot/tests/helpers.rs +++ b/ethcore/src/snapshot/tests/helpers.rs @@ -17,9 +17,9 @@ //! Snapshot test helpers. These are used to build blockchains and state tries //! which can be queried before and after a full snapshot/restore cycle. +use basic_account::BasicAccount; use account_db::AccountDBMut; use rand::Rng; -use snapshot::account::Account; use util::DBValue; use util::hash::{FixedHash, H256}; @@ -64,10 +64,10 @@ impl StateProducer { // sweep once to alter storage tries. for &mut (ref mut address_hash, ref mut account_data) in &mut accounts_to_modify { - let mut account = Account::from_thin_rlp(&*account_data); + let mut account: BasicAccount = ::rlp::decode(&*account_data); let acct_db = AccountDBMut::from_hash(db, *address_hash); - fill_storage(acct_db, account.storage_root_mut(), &mut self.storage_seed); - *account_data = DBValue::from_vec(account.to_thin_rlp()); + fill_storage(acct_db, &mut account.storage_root, &mut self.storage_seed); + *account_data = DBValue::from_vec(::rlp::encode(&account).to_vec()); } // sweep again to alter account trie. diff --git a/ethcore/src/snapshot/tests/state.rs b/ethcore/src/snapshot/tests/state.rs index 380e9fb0d..5fcc5a52c 100644 --- a/ethcore/src/snapshot/tests/state.rs +++ b/ethcore/src/snapshot/tests/state.rs @@ -16,8 +16,9 @@ //! State snapshotting tests. +use basic_account::BasicAccount; +use snapshot::account; use snapshot::{chunk_state, Error as SnapshotError, Progress, StateRebuilder}; -use snapshot::account::Account; use snapshot::io::{PackedReader, PackedWriter, SnapshotReader, SnapshotWriter}; use super::helpers::{compare_dbs, StateProducer}; @@ -113,22 +114,21 @@ fn get_code_from_prev_chunk() { // first one will have code inlined, // second will just have its hash. let thin_rlp = acc_stream.out(); - let acc1 = Account::from_thin_rlp(&thin_rlp); - let acc2 = Account::from_thin_rlp(&thin_rlp); + let acc: BasicAccount = ::rlp::decode(&thin_rlp); - let mut make_chunk = |acc: Account, hash| { + let mut make_chunk = |acc, hash| { let mut db = MemoryDB::new(); AccountDBMut::from_hash(&mut db, hash).insert(&code[..]); - let fat_rlp = acc.to_fat_rlp(&AccountDB::from_hash(&db, hash), &mut used_code).unwrap(); + let fat_rlp = account::to_fat_rlp(&acc, &AccountDB::from_hash(&db, hash), &mut used_code).unwrap(); let mut stream = RlpStream::new_list(1); stream.begin_list(2).append(&hash).append_raw(&fat_rlp, 1); stream.out() }; - let chunk1 = make_chunk(acc1, h1); - let chunk2 = make_chunk(acc2, h2); + let chunk1 = make_chunk(acc.clone(), h1); + let chunk2 = make_chunk(acc, h2); let db_path = RandomTempPath::create_dir(); let db_cfg = DatabaseConfig::with_columns(::db::NUM_COLUMNS); @@ -190,4 +190,4 @@ fn checks_flag() { } } } -} \ No newline at end of file +} From 8d1a91b3b8b7ccceb91c2d5311be49d1ad540dc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Wed, 4 Jan 2017 12:36:37 +0100 Subject: [PATCH 05/22] Removing orphaned Cargo.toml --- util/https-fetch/Cargo.toml | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 util/https-fetch/Cargo.toml diff --git a/util/https-fetch/Cargo.toml b/util/https-fetch/Cargo.toml deleted file mode 100644 index b45904f3a..000000000 --- a/util/https-fetch/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -description = "HTTPS fetching library" -homepage = "http://parity.io" -license = "GPL-3.0" -name = "https-fetch" -version = "0.1.0" -authors = ["Parity Technologies "] - -[dependencies] -log = "0.3" -ethabi = "0.2.2" -mio = { git = "https://github.com/ethcore/mio", branch = "v0.5.x" } -rustls = { git = "https://github.com/ctz/rustls" } -clippy = { version = "0.0.85", optional = true} - -[features] -default = [] -ca-github-only = [] -dev = ["clippy"] From 8ca0e09ffc42975291568f29015f5bcc597f575a Mon Sep 17 00:00:00 2001 From: maciejhirsz Date: Wed, 4 Jan 2017 12:50:50 +0100 Subject: [PATCH 06/22] Better error messages for PoA chains --- ethcore/src/account_provider/mod.rs | 5 +++++ parity/run.rs | 14 +++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/ethcore/src/account_provider/mod.rs b/ethcore/src/account_provider/mod.rs index 5b56c697c..2f810d6be 100644 --- a/ethcore/src/account_provider/mod.rs +++ b/ethcore/src/account_provider/mod.rs @@ -150,6 +150,11 @@ impl AccountProvider { Ok(Address::from(address).into()) } + /// Checks whether an account with a given address is present. + pub fn has_account(&self, address: Address) -> Result { + Ok(self.accounts()?.iter().any(|&a| a == address)) + } + /// Returns addresses of all accounts. pub fn accounts(&self) -> Result, Error> { let accounts = self.sstore.accounts()?; diff --git a/parity/run.rs b/parity/run.rs index 672c80fcf..2b36d6cd1 100644 --- a/parity/run.rs +++ b/parity/run.rs @@ -230,9 +230,21 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R miner.set_extra_data(cmd.miner_extras.extra_data); miner.set_transactions_limit(cmd.miner_extras.transactions_limit); let engine_signer = cmd.miner_extras.engine_signer; + if engine_signer != Default::default() { + // Check if engine signer exists + if !account_provider.has_account(engine_signer).unwrap_or(false) { + return Err(format!("Consensus signer account not found for the current chain, please run `parity account new -d current-d --chain current-chain`")); + } + + // Check if any passwords have been read from the password file(s) + if passwords.is_empty() { + return Err(format!("No password found for the consensus signer {}. Make sure valid password is present in files passed using `--password` or in the configuration file.", engine_signer)); + } + + // Attempt to sign in the engine signer. if !passwords.into_iter().any(|p| miner.set_engine_signer(engine_signer, p).is_ok()) { - return Err(format!("No password found for the consensus signer {}. Make sure valid password is present in files passed using `--password`.", engine_signer)); + return Err(format!("Invalid password for consensus signer {}. Make sure valid password is present in files passed using `--password` or in the configuration file.", engine_signer)); } } From 4b1b67bfeb370b14c728b8c4bae819ffb985e3ed Mon Sep 17 00:00:00 2001 From: maciejhirsz Date: Wed, 4 Jan 2017 14:05:32 +0100 Subject: [PATCH 07/22] Analog error messages for unlocking accounts --- parity/run.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/parity/run.rs b/parity/run.rs index 2b36d6cd1..ab9abface 100644 --- a/parity/run.rs +++ b/parity/run.rs @@ -234,7 +234,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R if engine_signer != Default::default() { // Check if engine signer exists if !account_provider.has_account(engine_signer).unwrap_or(false) { - return Err(format!("Consensus signer account not found for the current chain, please run `parity account new -d current-d --chain current-chain`")); + return Err("Consensus signer account not found for the current chain, please run `parity account new -d current-d --chain current-chain --keys-path current-keys-path`".to_owned()); } // Check if any passwords have been read from the password file(s) @@ -244,7 +244,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R // Attempt to sign in the engine signer. if !passwords.into_iter().any(|p| miner.set_engine_signer(engine_signer, p).is_ok()) { - return Err(format!("Invalid password for consensus signer {}. Make sure valid password is present in files passed using `--password` or in the configuration file.", engine_signer)); + return Err(format!("No valid password for the consensus signer {}. Make sure valid password is present in files passed using `--password` or in the configuration file.", engine_signer)); } } @@ -490,17 +490,27 @@ fn prepare_account_provider(dirs: &Directories, data_dir: &str, cfg: AccountsCon let path = dirs.keys_path(data_dir); upgrade_key_location(&dirs.legacy_keys_path(cfg.testnet), &path); let dir = Box::new(DiskDirectory::create(&path).map_err(|e| format!("Could not open keys directory: {}", e))?); - let account_service = AccountProvider::new(Box::new( + let account_provider = AccountProvider::new(Box::new( EthStore::open_with_iterations(dir, cfg.iterations).map_err(|e| format!("Could not open keys directory: {}", e))? )); for a in cfg.unlocked_accounts { - if !passwords.iter().any(|p| account_service.unlock_account_permanently(a, (*p).clone()).is_ok()) { - return Err(format!("No password found to unlock account {}. Make sure valid password is present in files passed using `--password`.", a)); + // Check if the account exists + if !account_provider.has_account(a).unwrap_or(false) { + return Err(format!("Account {} not found for the current chain, please run `parity account new -d current-d --chain current-chain --keys-path current-keys-path`", a)); + } + + // Check if any passwords have been read from the password file(s) + if passwords.is_empty() { + return Err(format!("No password found to unlock account {}. Make sure valid password is present in files passed using `--password` or in the configuration file.", a)); + } + + if !passwords.iter().any(|p| account_provider.unlock_account_permanently(a, (*p).clone()).is_ok()) { + return Err(format!("No valid password to unlock account {}. Make sure valid password is present in files passed using `--password` or in the configuration file.", a)); } } - Ok(account_service) + Ok(account_provider) } fn wait_for_exit( From 516c41c1ee1b92799dd8a3af2eb09b53e8cf746e Mon Sep 17 00:00:00 2001 From: maciejhirsz Date: Wed, 4 Jan 2017 14:48:32 +0100 Subject: [PATCH 08/22] Move hints to constants --- parity/run.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/parity/run.rs b/parity/run.rs index ab9abface..de9d7a639 100644 --- a/parity/run.rs +++ b/parity/run.rs @@ -60,6 +60,12 @@ const SNAPSHOT_PERIOD: u64 = 10000; // how many blocks to wait before starting a periodic snapshot. const SNAPSHOT_HISTORY: u64 = 100; +// Pops along with error message when an account address is not found. +const CREATE_ACCOUNT_HINT: &'static str = "Please run `parity account new [-d current-d --chain current-chain --keys-path current-keys-path]`."; + +// Pops along with error messages when a password is missing or invalid. +const VERIFY_PASSWORD_HINT: &'static str = "Make sure valid password is present in files passed using `--password` or in the configuration file."; + #[derive(Debug, PartialEq)] pub struct RunCmd { pub cache_config: CacheConfig, @@ -234,17 +240,17 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R if engine_signer != Default::default() { // Check if engine signer exists if !account_provider.has_account(engine_signer).unwrap_or(false) { - return Err("Consensus signer account not found for the current chain, please run `parity account new -d current-d --chain current-chain --keys-path current-keys-path`".to_owned()); + return Err(format!("Consensus signer account not found for the current chain. {}", CREATE_ACCOUNT_HINT)); } // Check if any passwords have been read from the password file(s) if passwords.is_empty() { - return Err(format!("No password found for the consensus signer {}. Make sure valid password is present in files passed using `--password` or in the configuration file.", engine_signer)); + return Err(format!("No password found for the consensus signer {}. {}", engine_signer, VERIFY_PASSWORD_HINT)); } // Attempt to sign in the engine signer. if !passwords.into_iter().any(|p| miner.set_engine_signer(engine_signer, p).is_ok()) { - return Err(format!("No valid password for the consensus signer {}. Make sure valid password is present in files passed using `--password` or in the configuration file.", engine_signer)); + return Err(format!("No valid password for the consensus signer {}. {}", engine_signer, VERIFY_PASSWORD_HINT)); } } @@ -497,16 +503,16 @@ fn prepare_account_provider(dirs: &Directories, data_dir: &str, cfg: AccountsCon for a in cfg.unlocked_accounts { // Check if the account exists if !account_provider.has_account(a).unwrap_or(false) { - return Err(format!("Account {} not found for the current chain, please run `parity account new -d current-d --chain current-chain --keys-path current-keys-path`", a)); + return Err(format!("Account {} not found for the current chain. {}", a, CREATE_ACCOUNT_HINT)); } // Check if any passwords have been read from the password file(s) if passwords.is_empty() { - return Err(format!("No password found to unlock account {}. Make sure valid password is present in files passed using `--password` or in the configuration file.", a)); + return Err(format!("No password found to unlock account {}. {}", a, VERIFY_PASSWORD_HINT)); } if !passwords.iter().any(|p| account_provider.unlock_account_permanently(a, (*p).clone()).is_ok()) { - return Err(format!("No valid password to unlock account {}. Make sure valid password is present in files passed using `--password` or in the configuration file.", a)); + return Err(format!("No valid password to unlock account {}. {}", a, VERIFY_PASSWORD_HINT)); } } From 63017268ad2da404f24e95ec5b059d3b3375afbc Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Wed, 4 Jan 2017 15:14:37 +0100 Subject: [PATCH 09/22] Add ownership checks the Registry dApp (#4001) * Fixes to the Registry dApp * WIP Add Owner Lookup * Proper sha3 implementation * Add working owner lookup to reg dApp * Add errors to Name Reg * Add records error in Reg dApp * Add errors for reverse in reg dApp * PR Grumbles --- js/package.json | 1 + js/src/api/util/sha3.js | 19 ++- js/src/dapps/registry.js | 13 ++ .../dapps/registry/Application/application.js | 4 +- js/src/dapps/registry/Lookup/actions.js | 44 +++++-- js/src/dapps/registry/Lookup/lookup.js | 116 ++++++++++++------ js/src/dapps/registry/Lookup/reducers.js | 2 +- js/src/dapps/registry/Names/actions.js | 80 +++++++----- js/src/dapps/registry/Names/names.css | 9 +- js/src/dapps/registry/Names/names.js | 59 +++++---- js/src/dapps/registry/Names/reducers.js | 37 ++++-- js/src/dapps/registry/Records/actions.js | 48 +++++--- js/src/dapps/registry/Records/records.css | 4 + js/src/dapps/registry/Records/records.js | 32 ++++- js/src/dapps/registry/Records/reducers.js | 17 ++- js/src/dapps/registry/Reverse/actions.js | 81 +++++++----- js/src/dapps/registry/Reverse/reducers.js | 21 +++- js/src/dapps/registry/Reverse/reverse.css | 3 + js/src/dapps/registry/Reverse/reverse.js | 32 ++++- js/src/dapps/registry/ui/address.js | 98 +++++++++------ js/src/dapps/registry/ui/image.js | 24 ++-- js/src/dapps/registry/util/actions.js | 4 +- js/src/dapps/registry/util/post-tx.js | 6 - js/src/dapps/registry/util/registry.js | 37 ++++++ .../transactionPendingFormConfirm.js | 5 +- js/src/views/Wallet/wallet.js | 2 +- 26 files changed, 577 insertions(+), 221 deletions(-) create mode 100644 js/src/dapps/registry/util/registry.js diff --git a/js/package.json b/js/package.json index 2ecad7269..8672578c7 100644 --- a/js/package.json +++ b/js/package.json @@ -138,6 +138,7 @@ "blockies": "0.0.2", "brace": "0.9.0", "bytes": "2.4.0", + "crypto-js": "3.1.9-1", "debounce": "1.0.0", "es6-error": "4.0.0", "es6-promise": "4.0.5", diff --git a/js/src/api/util/sha3.js b/js/src/api/util/sha3.js index 93b01d8dd..5a2c7c273 100644 --- a/js/src/api/util/sha3.js +++ b/js/src/api/util/sha3.js @@ -14,8 +14,21 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { keccak_256 } from 'js-sha3'; // eslint-disable-line camelcase +import CryptoJS from 'crypto-js'; +import CryptoSha3 from 'crypto-js/sha3'; -export function sha3 (value) { - return `0x${keccak_256(value)}`; +export function sha3 (value, options) { + if (options && options.encoding === 'hex') { + if (value.length > 2 && value.substr(0, 2) === '0x') { + value = value.substr(2); + } + + value = CryptoJS.enc.Hex.parse(value); + } + + const hash = CryptoSha3(value, { + outputLength: 256 + }).toString(); + + return `0x${hash}`; } diff --git a/js/src/dapps/registry.js b/js/src/dapps/registry.js index 0b8a8be55..3723bd9fa 100644 --- a/js/src/dapps/registry.js +++ b/js/src/dapps/registry.js @@ -34,3 +34,16 @@ ReactDOM.render( , document.querySelector('#container') ); + +if (module.hot) { + module.hot.accept('./registry/Container', () => { + require('./registry/Container'); + + ReactDOM.render( + + + , + document.querySelector('#container') + ); + }); +} diff --git a/js/src/dapps/registry/Application/application.js b/js/src/dapps/registry/Application/application.js index abfacc497..a1d5c0ef2 100644 --- a/js/src/dapps/registry/Application/application.js +++ b/js/src/dapps/registry/Application/application.js @@ -44,8 +44,8 @@ export default class Application extends Component { static propTypes = { accounts: PropTypes.object.isRequired, - contract: nullableProptype(PropTypes.object).isRequired, - fee: nullableProptype(PropTypes.object).isRequired + contract: nullableProptype(PropTypes.object.isRequired), + fee: nullableProptype(PropTypes.object.isRequired) }; render () { diff --git a/js/src/dapps/registry/Lookup/actions.js b/js/src/dapps/registry/Lookup/actions.js index eb1c7db66..1e8ed5898 100644 --- a/js/src/dapps/registry/Lookup/actions.js +++ b/js/src/dapps/registry/Lookup/actions.js @@ -15,11 +15,13 @@ // along with Parity. If not, see . import { sha3 } from '../parity.js'; +import { getOwner } from '../util/registry'; export const clear = () => ({ type: 'lookup clear' }); export const lookupStart = (name, key) => ({ type: 'lookup start', name, key }); export const reverseLookupStart = (address) => ({ type: 'reverseLookup start', address }); +export const ownerLookupStart = (name) => ({ type: 'ownerLookup start', name }); export const success = (action, result) => ({ type: `${action} success`, result: result }); @@ -48,24 +50,50 @@ export const lookup = (name, key) => (dispatch, getState) => { }); }; -export const reverseLookup = (address) => (dispatch, getState) => { +export const reverseLookup = (lookupAddress) => (dispatch, getState) => { const { contract } = getState(); + if (!contract) { return; } - const reverse = contract.functions - .find((f) => f.name === 'reverse'); + dispatch(reverseLookupStart(lookupAddress)); - dispatch(reverseLookupStart(address)); - - reverse.call({}, [ address ]) - .then((address) => dispatch(success('reverseLookup', address))) + contract.instance + .reverse + .call({}, [ lookupAddress ]) + .then((address) => { + dispatch(success('reverseLookup', address)); + }) .catch((err) => { - console.error(`could not lookup reverse for ${address}`); + console.error(`could not lookup reverse for ${lookupAddress}`); if (err) { console.error(err.stack); } dispatch(fail('reverseLookup')); }); }; + +export const ownerLookup = (name) => (dispatch, getState) => { + const { contract } = getState(); + + if (!contract) { + return; + } + + dispatch(ownerLookupStart(name)); + + return getOwner(contract, name) + .then((owner) => { + dispatch(success('ownerLookup', owner)); + }) + .catch((err) => { + console.error(`could not lookup owner for ${name}`); + + if (err) { + console.error(err.stack); + } + + dispatch(fail('ownerLookup')); + }); +}; diff --git a/js/src/dapps/registry/Lookup/lookup.js b/js/src/dapps/registry/Lookup/lookup.js index bf01df115..f572cbb7d 100644 --- a/js/src/dapps/registry/Lookup/lookup.js +++ b/js/src/dapps/registry/Lookup/lookup.js @@ -23,13 +23,14 @@ import DropDownMenu from 'material-ui/DropDownMenu'; import MenuItem from 'material-ui/MenuItem'; import RaisedButton from 'material-ui/RaisedButton'; import SearchIcon from 'material-ui/svg-icons/action/search'; +import keycode from 'keycode'; import { nullableProptype } from '~/util/proptypes'; import Address from '../ui/address.js'; import renderImage from '../ui/image.js'; -import { clear, lookup, reverseLookup } from './actions'; +import { clear, lookup, ownerLookup, reverseLookup } from './actions'; import styles from './lookup.css'; class Lookup extends Component { @@ -39,6 +40,7 @@ class Lookup extends Component { clear: PropTypes.func.isRequired, lookup: PropTypes.func.isRequired, + ownerLookup: PropTypes.func.isRequired, reverseLookup: PropTypes.func.isRequired } @@ -50,33 +52,6 @@ class Lookup extends Component { const { input, type } = this.state; const { result } = this.props; - let output = ''; - if (result) { - if (type === 'A') { - output = ( - -
- - ); - } else if (type === 'IMG') { - output = renderImage(result); - } else if (type === 'CONTENT') { - output = ( -
- { result } -

Keep in mind that this is most likely the hash of the content you are looking for.

-
- ); - } else { - output = ( - { result } - ); - } - } - return ( @@ -85,6 +60,7 @@ class Lookup extends Component { hintText={ type === 'reverse' ? 'address' : 'name' } value={ input } onChange={ this.onInputChange } + onKeyDown={ this.onKeyDown } /> + - { output } + + { this.renderOutput(type, result) } + ); } + renderOutput (type, result) { + if (result === null) { + return null; + } + + if (type === 'A') { + return ( + +
+ + ); + } + + if (type === 'owner') { + if (!result) { + return ( + Not reserved yet + ); + } + + return ( + +
+ + ); + } + + if (type === 'IMG') { + return renderImage(result); + } + + if (type === 'CONTENT') { + return ( +
+ { result } +

Keep in mind that this is most likely the hash of the content you are looking for.

+
+ ); + } + + return ( + { result || 'No data' } + ); + } + onInputChange = (e) => { this.setState({ input: e.target.value }); - }; + } + + onKeyDown = (event) => { + const codeName = keycode(event); + + if (codeName !== 'enter') { + return; + } + + this.onLookupClick(); + } onTypeChange = (e, i, type) => { this.setState({ type }); this.props.clear(); - }; + } onLookupClick = () => { const { input, type } = this.state; if (type === 'reverse') { - this.props.reverseLookup(input); - } else { - this.props.lookup(input, type); + return this.props.reverseLookup(input); } - }; + + if (type === 'owner') { + return this.props.ownerLookup(input); + } + + return this.props.lookup(input, type); + } } const mapStateToProps = (state) => state.lookup; const mapDispatchToProps = (dispatch) => bindActionCreators({ - clear, lookup, reverseLookup + clear, lookup, ownerLookup, reverseLookup }, dispatch); export default connect(mapStateToProps, mapDispatchToProps)(Lookup); diff --git a/js/src/dapps/registry/Lookup/reducers.js b/js/src/dapps/registry/Lookup/reducers.js index b675fc702..8c804c28e 100644 --- a/js/src/dapps/registry/Lookup/reducers.js +++ b/js/src/dapps/registry/Lookup/reducers.js @@ -24,7 +24,7 @@ const initialState = { export default (state = initialState, action) => { const { type } = action; - if (type.slice(0, 7) !== 'lookup ' && type.slice(0, 14) !== 'reverseLookup ') { + if (!/^(lookup|reverseLookup|ownerLookup)/.test(type)) { return state; } diff --git a/js/src/dapps/registry/Names/actions.js b/js/src/dapps/registry/Names/actions.js index 67867ca8e..2396278cb 100644 --- a/js/src/dapps/registry/Names/actions.js +++ b/js/src/dapps/registry/Names/actions.js @@ -15,8 +15,13 @@ // along with Parity. If not, see . import { sha3, api } from '../parity.js'; +import { getOwner, isOwned } from '../util/registry'; import postTx from '../util/post-tx'; +export const clearError = () => ({ + type: 'clearError' +}); + const alreadyQueued = (queue, action, name) => !!queue.find((entry) => entry.action === action && entry.name === name); @@ -24,13 +29,14 @@ export const reserveStart = (name) => ({ type: 'names reserve start', name }); export const reserveSuccess = (name) => ({ type: 'names reserve success', name }); -export const reserveFail = (name) => ({ type: 'names reserve fail', name }); +export const reserveFail = (name, error) => ({ type: 'names reserve fail', name, error }); export const reserve = (name) => (dispatch, getState) => { const state = getState(); const account = state.accounts.selected; const contract = state.contract; const fee = state.fee; + if (!contract || !account) { return; } @@ -40,27 +46,34 @@ export const reserve = (name) => (dispatch, getState) => { if (alreadyQueued(state.names.queue, 'reserve', name)) { return; } - const reserve = contract.functions.find((f) => f.name === 'reserve'); dispatch(reserveStart(name)); - const options = { - from: account.address, - value: fee - }; - const values = [ - sha3(name) - ]; + return isOwned(contract, name) + .then((owned) => { + if (owned) { + throw new Error(`"${name}" has already been reserved`); + } - postTx(api, reserve, options, values) + const { reserve } = contract.instance; + + const options = { + from: account.address, + value: fee + }; + const values = [ + sha3(name) + ]; + + return postTx(api, reserve, options, values); + }) .then((txHash) => { dispatch(reserveSuccess(name)); }) .catch((err) => { - console.error(`could not reserve ${name}`); - - if (err) { - console.error(err.stack); + if (err.type !== 'REQUEST_REJECTED') { + console.error(`error rerserving ${name}`, err); + return dispatch(reserveFail(name, err)); } dispatch(reserveFail(name)); @@ -71,43 +84,52 @@ export const dropStart = (name) => ({ type: 'names drop start', name }); export const dropSuccess = (name) => ({ type: 'names drop success', name }); -export const dropFail = (name) => ({ type: 'names drop fail', name }); +export const dropFail = (name, error) => ({ type: 'names drop fail', name, error }); export const drop = (name) => (dispatch, getState) => { const state = getState(); const account = state.accounts.selected; const contract = state.contract; + if (!contract || !account) { return; } name = name.toLowerCase(); + if (alreadyQueued(state.names.queue, 'drop', name)) { return; } - const drop = contract.functions.find((f) => f.name === 'drop'); - dispatch(dropStart(name)); - const options = { - from: account.address - }; - const values = [ - sha3(name) - ]; + return getOwner(contract, name) + .then((owner) => { + if (owner.toLowerCase() !== account.address.toLowerCase()) { + throw new Error(`you are not the owner of "${name}"`); + } - postTx(api, drop, options, values) + const { drop } = contract.instance; + + const options = { + from: account.address + }; + + const values = [ + sha3(name) + ]; + + return postTx(api, drop, options, values); + }) .then((txhash) => { dispatch(dropSuccess(name)); }) .catch((err) => { - console.error(`could not drop ${name}`); - - if (err) { - console.error(err.stack); + if (err.type !== 'REQUEST_REJECTED') { + console.error(`error dropping ${name}`, err); + return dispatch(dropFail(name, err)); } - dispatch(reserveFail(name)); + dispatch(dropFail(name)); }); }; diff --git a/js/src/dapps/registry/Names/names.css b/js/src/dapps/registry/Names/names.css index b56387909..46d6a5560 100644 --- a/js/src/dapps/registry/Names/names.css +++ b/js/src/dapps/registry/Names/names.css @@ -35,7 +35,12 @@ .link { color: #00BCD4; text-decoration: none; + + &:hover { + text-decoration: underline; + } } -.link:hover { - text-decoration: underline; + +.error { + color: red; } diff --git a/js/src/dapps/registry/Names/names.js b/js/src/dapps/registry/Names/names.js index c3f0e79f6..c34e172b9 100644 --- a/js/src/dapps/registry/Names/names.js +++ b/js/src/dapps/registry/Names/names.js @@ -24,9 +24,10 @@ import MenuItem from 'material-ui/MenuItem'; import RaisedButton from 'material-ui/RaisedButton'; import CheckIcon from 'material-ui/svg-icons/navigation/check'; +import { nullableProptype } from '~/util/proptypes'; import { fromWei } from '../parity.js'; -import { reserve, drop } from './actions'; +import { clearError, reserve, drop } from './actions'; import styles from './names.css'; const useSignerText = (

Use the Signer to authenticate the following changes.

); @@ -78,35 +79,21 @@ const renderQueue = (queue) => { class Names extends Component { static propTypes = { + error: nullableProptype(PropTypes.object.isRequired), fee: PropTypes.object.isRequired, pending: PropTypes.bool.isRequired, queue: PropTypes.array.isRequired, + clearError: PropTypes.func.isRequired, reserve: PropTypes.func.isRequired, drop: PropTypes.func.isRequired - } + }; state = { action: 'reserve', name: '' }; - componentWillReceiveProps (nextProps) { - const nextQueue = nextProps.queue; - const prevQueue = this.props.queue; - - if (nextQueue.length > prevQueue.length) { - const newQueued = nextQueue[nextQueue.length - 1]; - const newName = newQueued.name; - - if (newName !== this.state.name) { - return; - } - - this.setState({ name: '' }); - } - } - render () { const { action, name } = this.state; const { fee, pending, queue } = this.props; @@ -122,6 +109,7 @@ class Names extends Component { : (

To drop a name, you have to be the owner.

) ) } + { this.renderError() }
+ { error.message } +
+ ); + } + onNameChange = (e) => { + this.clearError(); this.setState({ name: e.target.value }); }; + onActionChange = (e, i, action) => { + this.clearError(); this.setState({ action }); }; + onSubmitClick = () => { const { action, name } = this.state; + if (action === 'reserve') { - this.props.reserve(name); - } else if (action === 'drop') { - this.props.drop(name); + return this.props.reserve(name); + } + + if (action === 'drop') { + return this.props.drop(name); + } + }; + + clearError = () => { + if (this.props.error) { + this.props.clearError(); } }; } const mapStateToProps = (state) => ({ ...state.names, fee: state.fee }); -const mapDispatchToProps = (dispatch) => bindActionCreators({ reserve, drop }, dispatch); +const mapDispatchToProps = (dispatch) => bindActionCreators({ clearError, reserve, drop }, dispatch); export default connect(mapStateToProps, mapDispatchToProps)(Names); diff --git a/js/src/dapps/registry/Names/reducers.js b/js/src/dapps/registry/Names/reducers.js index 17230ad40..461718a58 100644 --- a/js/src/dapps/registry/Names/reducers.js +++ b/js/src/dapps/registry/Names/reducers.js @@ -17,32 +17,55 @@ import { isAction, isStage, addToQueue, removeFromQueue } from '../util/actions'; const initialState = { + error: null, pending: false, queue: [] }; export default (state = initialState, action) => { + switch (action.type) { + case 'clearError': + return { + ...state, + error: null + }; + } + if (isAction('names', 'reserve', action)) { if (isStage('start', action)) { return { - ...state, pending: true, + ...state, + error: null, + pending: true, queue: addToQueue(state.queue, 'reserve', action.name) }; - } else if (isStage('success', action) || isStage('fail', action)) { + } + + if (isStage('success', action) || isStage('fail', action)) { return { - ...state, pending: false, + ...state, + error: action.error || null, + pending: false, queue: removeFromQueue(state.queue, 'reserve', action.name) }; } - } else if (isAction('names', 'drop', action)) { + } + + if (isAction('names', 'drop', action)) { if (isStage('start', action)) { return { - ...state, pending: true, + ...state, + error: null, + pending: true, queue: addToQueue(state.queue, 'drop', action.name) }; - } else if (isStage('success', action) || isStage('fail', action)) { + } + + if (isStage('success', action) || isStage('fail', action)) { return { - ...state, pending: false, + ...state, + error: action.error || null, + pending: false, queue: removeFromQueue(state.queue, 'drop', action.name) }; } diff --git a/js/src/dapps/registry/Records/actions.js b/js/src/dapps/registry/Records/actions.js index 9afcb172c..f85304d5f 100644 --- a/js/src/dapps/registry/Records/actions.js +++ b/js/src/dapps/registry/Records/actions.js @@ -16,45 +16,57 @@ import { sha3, api } from '../parity.js'; import postTx from '../util/post-tx'; +import { getOwner } from '../util/registry'; + +export const clearError = () => ({ + type: 'clearError' +}); export const start = (name, key, value) => ({ type: 'records update start', name, key, value }); export const success = () => ({ type: 'records update success' }); -export const fail = () => ({ type: 'records update error' }); +export const fail = (error) => ({ type: 'records update fail', error }); export const update = (name, key, value) => (dispatch, getState) => { const state = getState(); const account = state.accounts.selected; const contract = state.contract; + if (!contract || !account) { return; } name = name.toLowerCase(); - - const fnName = key === 'A' ? 'setAddress' : 'set'; - const setAddress = contract.functions.find((f) => f.name === fnName); - dispatch(start(name, key, value)); - const options = { - from: account.address - }; - const values = [ - sha3(name), - key, - value - ]; + return getOwner(contract, name) + .then((owner) => { + if (owner.toLowerCase() !== account.address.toLowerCase()) { + throw new Error(`you are not the owner of "${name}"`); + } - postTx(api, setAddress, options, values) + const fnName = key === 'A' ? 'setAddress' : 'set'; + const method = contract.instance[fnName]; + + const options = { + from: account.address + }; + + const values = [ + sha3(name), + key, + value + ]; + + return postTx(api, method, options, values); + }) .then((txHash) => { dispatch(success()); }).catch((err) => { - console.error(`could not update ${key} record of ${name}`); - - if (err) { - console.error(err.stack); + if (err.type !== 'REQUEST_REJECTED') { + console.error(`error updating ${name}`, err); + return dispatch(fail(err)); } dispatch(fail()); diff --git a/js/src/dapps/registry/Records/records.css b/js/src/dapps/registry/Records/records.css index e16ea4a15..03af5801f 100644 --- a/js/src/dapps/registry/Records/records.css +++ b/js/src/dapps/registry/Records/records.css @@ -36,3 +36,7 @@ flex-grow: 0; flex-shrink: 0; } + +.error { + color: red; +} diff --git a/js/src/dapps/registry/Records/records.js b/js/src/dapps/registry/Records/records.js index f1d92cac8..f9c9cea76 100644 --- a/js/src/dapps/registry/Records/records.js +++ b/js/src/dapps/registry/Records/records.js @@ -24,17 +24,20 @@ import MenuItem from 'material-ui/MenuItem'; import RaisedButton from 'material-ui/RaisedButton'; import SaveIcon from 'material-ui/svg-icons/content/save'; -import { update } from './actions'; +import { nullableProptype } from '~/util/proptypes'; +import { clearError, update } from './actions'; import styles from './records.css'; class Records extends Component { static propTypes = { + error: nullableProptype(PropTypes.object.isRequired), pending: PropTypes.bool.isRequired, name: PropTypes.string.isRequired, type: PropTypes.string.isRequired, value: PropTypes.string.isRequired, + clearError: PropTypes.func.isRequired, update: PropTypes.func.isRequired } @@ -53,6 +56,7 @@ class Records extends Component {

You can only modify entries of names that you previously registered.

+ { this.renderError() }
+ { error.message } +
+ ); + } + onNameChange = (e) => { + this.clearError(); this.setState({ name: e.target.value }); }; + onTypeChange = (e, i, type) => { this.setState({ type }); }; + onValueChange = (e) => { this.setState({ value: e.target.value }); }; + onSaveClick = () => { const { name, type, value } = this.state; this.props.update(name, type, value); }; + + clearError = () => { + if (this.props.error) { + this.props.clearError(); + } + }; } const mapStateToProps = (state) => state.records; -const mapDispatchToProps = (dispatch) => bindActionCreators({ update }, dispatch); +const mapDispatchToProps = (dispatch) => bindActionCreators({ clearError, update }, dispatch); export default connect(mapStateToProps, mapDispatchToProps)(Records); diff --git a/js/src/dapps/registry/Records/reducers.js b/js/src/dapps/registry/Records/reducers.js index 9629e8149..2dd45c012 100644 --- a/js/src/dapps/registry/Records/reducers.js +++ b/js/src/dapps/registry/Records/reducers.js @@ -17,11 +17,20 @@ import { isAction, isStage } from '../util/actions'; const initialState = { + error: null, pending: false, name: '', type: '', value: '' }; export default (state = initialState, action) => { + switch (action.type) { + case 'clearError': + return { + ...state, + error: null + }; + } + if (!isAction('records', 'update', action)) { return state; } @@ -29,11 +38,15 @@ export default (state = initialState, action) => { if (isStage('start', action)) { return { ...state, pending: true, - name: action.name, type: action.entry, value: action.value + error: null, + name: action.name, type: action.key, value: action.value }; - } else if (isStage('success', action) || isStage('fail', action)) { + } + + if (isStage('success', action) || isStage('fail', action)) { return { ...state, pending: false, + error: action.error || null, name: initialState.name, type: initialState.type, value: initialState.value }; } diff --git a/js/src/dapps/registry/Reverse/actions.js b/js/src/dapps/registry/Reverse/actions.js index 07a1afade..bccd60f2f 100644 --- a/js/src/dapps/registry/Reverse/actions.js +++ b/js/src/dapps/registry/Reverse/actions.js @@ -16,44 +16,58 @@ import { api } from '../parity.js'; import postTx from '../util/post-tx'; +import { getOwner } from '../util/registry'; + +export const clearError = () => ({ + type: 'clearError' +}); export const start = (action, name, address) => ({ type: `reverse ${action} start`, name, address }); export const success = (action) => ({ type: `reverse ${action} success` }); -export const fail = (action) => ({ type: `reverse ${action} error` }); +export const fail = (action, error) => ({ type: `reverse ${action} fail`, error }); export const propose = (name, address) => (dispatch, getState) => { const state = getState(); const account = state.accounts.selected; const contract = state.contract; + if (!contract || !account) { return; } name = name.toLowerCase(); - - const proposeReverse = contract.functions.find((f) => f.name === 'proposeReverse'); - dispatch(start('propose', name, address)); - const options = { - from: account.address - }; - const values = [ - name, - address - ]; + return getOwner(contract, name) + .then((owner) => { + if (owner.toLowerCase() !== account.address.toLowerCase()) { + throw new Error(`you are not the owner of "${name}"`); + } - postTx(api, proposeReverse, options, values) + const { proposeReverse } = contract.instance; + + const options = { + from: account.address + }; + + const values = [ + name, + address + ]; + + return postTx(api, proposeReverse, options, values); + }) .then((txHash) => { dispatch(success('propose')); }) .catch((err) => { - console.error(`could not propose reverse ${name} for address ${address}`); - if (err) { - console.error(err.stack); + if (err.type !== 'REQUEST_REJECTED') { + console.error(`error proposing ${name}`, err); + return dispatch(fail('propose', err)); } + dispatch(fail('propose')); }); }; @@ -62,31 +76,42 @@ export const confirm = (name) => (dispatch, getState) => { const state = getState(); const account = state.accounts.selected; const contract = state.contract; + if (!contract || !account) { return; } + name = name.toLowerCase(); - - const confirmReverse = contract.functions.find((f) => f.name === 'confirmReverse'); - dispatch(start('confirm', name)); - const options = { - from: account.address - }; - const values = [ - name - ]; + return getOwner(contract, name) + .then((owner) => { + if (owner.toLowerCase() !== account.address.toLowerCase()) { + throw new Error(`you are not the owner of "${name}"`); + } - postTx(api, confirmReverse, options, values) + const { confirmReverse } = contract.instance; + + const options = { + from: account.address + }; + + const values = [ + name + ]; + + return postTx(api, confirmReverse, options, values); + }) .then((txHash) => { dispatch(success('confirm')); }) .catch((err) => { - console.error(`could not confirm reverse ${name}`); - if (err) { - console.error(err.stack); + if (err.type !== 'REQUEST_REJECTED') { + console.error(`error confirming ${name}`, err); + return dispatch(fail('confirm', err)); } + dispatch(fail('confirm')); }); }; + diff --git a/js/src/dapps/registry/Reverse/reducers.js b/js/src/dapps/registry/Reverse/reducers.js index 53a242c3b..f7ba65648 100644 --- a/js/src/dapps/registry/Reverse/reducers.js +++ b/js/src/dapps/registry/Reverse/reducers.js @@ -17,24 +17,37 @@ import { isAction, isStage } from '../util/actions'; const initialState = { + error: null, pending: false, queue: [] }; export default (state = initialState, action) => { + switch (action.type) { + case 'clearError': + return { + ...state, + error: null + }; + } + if (isAction('reverse', 'propose', action)) { if (isStage('start', action)) { return { ...state, pending: true, + error: null, queue: state.queue.concat({ action: 'propose', name: action.name, address: action.address }) }; - } else if (isStage('success', action) || isStage('fail', action)) { + } + + if (isStage('success', action) || isStage('fail', action)) { return { ...state, pending: false, + error: action.error || null, queue: state.queue.filter((e) => e.action === 'propose' && e.name === action.name && @@ -48,14 +61,18 @@ export default (state = initialState, action) => { if (isStage('start', action)) { return { ...state, pending: true, + error: null, queue: state.queue.concat({ action: 'confirm', name: action.name }) }; - } else if (isStage('success', action) || isStage('fail', action)) { + } + + if (isStage('success', action) || isStage('fail', action)) { return { ...state, pending: false, + error: action.error || null, queue: state.queue.filter((e) => e.action === 'confirm' && e.name === action.name diff --git a/js/src/dapps/registry/Reverse/reverse.css b/js/src/dapps/registry/Reverse/reverse.css index 0b75bfaf4..b7e5f64cb 100644 --- a/js/src/dapps/registry/Reverse/reverse.css +++ b/js/src/dapps/registry/Reverse/reverse.css @@ -37,3 +37,6 @@ flex-shrink: 0; } +.error { + color: red; +} diff --git a/js/src/dapps/registry/Reverse/reverse.js b/js/src/dapps/registry/Reverse/reverse.js index 24af0a7a4..0216d00a2 100644 --- a/js/src/dapps/registry/Reverse/reverse.js +++ b/js/src/dapps/registry/Reverse/reverse.js @@ -21,17 +21,20 @@ import { Card, CardHeader, CardText, TextField, DropDownMenu, MenuItem, RaisedButton } from 'material-ui'; +import { nullableProptype } from '~/util/proptypes'; import { AddIcon, CheckIcon } from '~/ui/Icons'; -import { propose, confirm } from './actions'; +import { clearError, confirm, propose } from './actions'; import styles from './reverse.css'; class Reverse extends Component { static propTypes = { + error: nullableProptype(PropTypes.object.isRequired), pending: PropTypes.bool.isRequired, queue: PropTypes.array.isRequired, - propose: PropTypes.func.isRequired, - confirm: PropTypes.func.isRequired + clearError: PropTypes.func.isRequired, + confirm: PropTypes.func.isRequired, + propose: PropTypes.func.isRequired } state = { @@ -77,6 +80,7 @@ class Reverse extends Component {

{ explanation } + { this.renderError() }
+ { error.message } +
+ ); + } + onNameChange = (e) => { this.setState({ name: e.target.value }); }; @@ -129,9 +147,15 @@ class Reverse extends Component { this.props.confirm(name); } }; + + clearError = () => { + if (this.props.error) { + this.props.clearError(); + } + }; } const mapStateToProps = (state) => state.reverse; -const mapDispatchToProps = (dispatch) => bindActionCreators({ propose, confirm }, dispatch); +const mapDispatchToProps = (dispatch) => bindActionCreators({ clearError, confirm, propose }, dispatch); export default connect(mapStateToProps, mapDispatchToProps)(Reverse); diff --git a/js/src/dapps/registry/ui/address.js b/js/src/dapps/registry/ui/address.js index e3eac2c97..d8e98c220 100644 --- a/js/src/dapps/registry/ui/address.js +++ b/js/src/dapps/registry/ui/address.js @@ -20,31 +20,48 @@ import { connect } from 'react-redux'; import Hash from './hash'; import etherscanUrl from '../util/etherscan-url'; import IdentityIcon from '../IdentityIcon'; +import { nullableProptype } from '~/util/proptypes'; import styles from './address.css'; class Address extends Component { static propTypes = { address: PropTypes.string.isRequired, - accounts: PropTypes.object.isRequired, - contacts: PropTypes.object.isRequired, + account: nullableProptype(PropTypes.object.isRequired), isTestnet: PropTypes.bool.isRequired, key: PropTypes.string, shortenHash: PropTypes.bool - } + }; static defaultProps = { key: 'address', shortenHash: true - } + }; render () { - const { address, accounts, contacts, isTestnet, key, shortenHash } = this.props; + const { address, key } = this.props; - let caption; - if (accounts[address] || contacts[address]) { - const name = (accounts[address] || contacts[address] || {}).name; - caption = ( + return ( +
+ + { this.renderCaption() } +
+ ); + } + + renderCaption () { + const { address, account, isTestnet, shortenHash } = this.props; + + if (account) { + const { name } = account; + + return ( ); - } else { - caption = ( - - { shortenHash ? ( - - ) : address } - - ); } return ( -
- - { caption } -
+ + { shortenHash ? ( + + ) : address } + ); } } +function mapStateToProps (initState, initProps) { + const { accounts, contacts } = initState; + + const allAccounts = Object.assign({}, accounts.all, contacts); + + // Add lower case addresses to map + Object + .keys(allAccounts) + .forEach((address) => { + allAccounts[address.toLowerCase()] = allAccounts[address]; + }); + + return (state, props) => { + const { isTestnet } = state; + const { address = '' } = props; + + const account = allAccounts[address] || null; + + return { + account, + isTestnet + }; + }; +} + export default connect( - // mapStateToProps - (state) => ({ - accounts: state.accounts.all, - contacts: state.contacts, - isTestnet: state.isTestnet - }), - // mapDispatchToProps - null + mapStateToProps )(Address); diff --git a/js/src/dapps/registry/ui/image.js b/js/src/dapps/registry/ui/image.js index c66e34128..c7774bfac 100644 --- a/js/src/dapps/registry/ui/image.js +++ b/js/src/dapps/registry/ui/image.js @@ -23,10 +23,20 @@ const styles = { border: '1px solid #777' }; -export default (address) => ( - { -); +export default (address) => { + if (!address || /^(0x)?0*$/.test(address)) { + return ( + + No image + + ); + } + + return ( + { + ); +}; diff --git a/js/src/dapps/registry/util/actions.js b/js/src/dapps/registry/util/actions.js index 0f4f350fc..1ae7426de 100644 --- a/js/src/dapps/registry/util/actions.js +++ b/js/src/dapps/registry/util/actions.js @@ -19,7 +19,7 @@ export const isAction = (ns, type, action) => { }; export const isStage = (stage, action) => { - return action.type.slice(-1 - stage.length) === ` ${stage}`; + return (new RegExp(`${stage}$`)).test(action.type); }; export const addToQueue = (queue, action, name) => { @@ -27,5 +27,5 @@ export const addToQueue = (queue, action, name) => { }; export const removeFromQueue = (queue, action, name) => { - return queue.filter((e) => e.action === action && e.name === name); + return queue.filter((e) => !(e.action === action && e.name === name)); }; diff --git a/js/src/dapps/registry/util/post-tx.js b/js/src/dapps/registry/util/post-tx.js index 84326dcab..298bbd843 100644 --- a/js/src/dapps/registry/util/post-tx.js +++ b/js/src/dapps/registry/util/post-tx.js @@ -24,12 +24,6 @@ const postTx = (api, method, opt = {}, values = []) => { }) .then((reqId) => { return api.pollMethod('parity_checkRequest', reqId); - }) - .catch((err) => { - if (err && err.type === 'REQUEST_REJECTED') { - throw new Error('The request has been rejected.'); - } - throw err; }); }; diff --git a/js/src/dapps/registry/util/registry.js b/js/src/dapps/registry/util/registry.js new file mode 100644 index 000000000..371b29aec --- /dev/null +++ b/js/src/dapps/registry/util/registry.js @@ -0,0 +1,37 @@ +// Copyright 2015, 2016 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 const getOwner = (contract, name) => { + const { address, api } = contract; + + const key = api.util.sha3(name) + '0000000000000000000000000000000000000000000000000000000000000001'; + const position = api.util.sha3(key, { encoding: 'hex' }); + + return api + .eth + .getStorageAt(address, position) + .then((result) => { + if (/^(0x)?0*$/.test(result)) { + return ''; + } + + return '0x' + result.slice(-40); + }); +}; + +export const isOwned = (contract, name) => { + return getOwner(contract, name).then((owner) => !!owner); +}; diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js index 02b7ef266..99bd1c5f3 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js +++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js @@ -20,6 +20,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import RaisedButton from 'material-ui/RaisedButton'; import ReactTooltip from 'react-tooltip'; +import keycode from 'keycode'; import { Form, Input, IdentityIcon } from '~/ui'; @@ -207,7 +208,9 @@ class TransactionPendingFormConfirm extends Component { } onKeyDown = (event) => { - if (event.which !== 13) { + const codeName = keycode(event); + + if (codeName !== 'enter') { return; } diff --git a/js/src/views/Wallet/wallet.js b/js/src/views/Wallet/wallet.js index 5fe6c957e..5418448b4 100644 --- a/js/src/views/Wallet/wallet.js +++ b/js/src/views/Wallet/wallet.js @@ -71,7 +71,7 @@ class Wallet extends Component { owned: PropTypes.bool.isRequired, setVisibleAccounts: PropTypes.func.isRequired, wallet: PropTypes.object.isRequired, - walletAccount: nullableProptype(PropTypes.object).isRequired + walletAccount: nullableProptype(PropTypes.object.isRequired) }; state = { From 7cdfaf1a439a5287b55b4195b29878193f0d7049 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Wed, 4 Jan 2017 15:14:51 +0100 Subject: [PATCH 10/22] Unsubscribe error on ShapeShift modal close (#4005) * unsubscribe in onClose (state available) * Revert "unsubscribe in onClose (state available)" This reverts commit 1da0a7447563e3cb0d9149b0b9898ec93b483982. * Fix shapeshift double unsubscribe * Swap multiple list test addresses --- js/src/3rdparty/shapeshift/helpers.spec.js | 18 +-- js/src/3rdparty/shapeshift/rpc.spec.js | 19 ++- js/src/3rdparty/shapeshift/shapeshift.js | 77 +++++++----- js/src/3rdparty/shapeshift/shapeshift.spec.js | 110 ++++++++++++++++-- 4 files changed, 168 insertions(+), 56 deletions(-) diff --git a/js/src/3rdparty/shapeshift/helpers.spec.js b/js/src/3rdparty/shapeshift/helpers.spec.js index 8ccec6791..d5e9994b8 100644 --- a/js/src/3rdparty/shapeshift/helpers.spec.js +++ b/js/src/3rdparty/shapeshift/helpers.spec.js @@ -16,16 +16,10 @@ const nock = require('nock'); -const ShapeShift = require('./'); -const initShapeshift = (ShapeShift.default || ShapeShift); - const APIKEY = '0x123454321'; -const shapeshift = initShapeshift(APIKEY); -const rpc = shapeshift.getRpc(); - -function mockget (requests) { - let scope = nock(rpc.ENDPOINT); +function mockget (shapeshift, requests) { + let scope = nock(shapeshift.getRpc().ENDPOINT); requests.forEach((request) => { scope = scope @@ -38,8 +32,8 @@ function mockget (requests) { return scope; } -function mockpost (requests) { - let scope = nock(rpc.ENDPOINT); +function mockpost (shapeshift, requests) { + let scope = nock(shapeshift.getRpc().ENDPOINT); requests.forEach((request) => { scope = scope @@ -58,7 +52,5 @@ function mockpost (requests) { module.exports = { APIKEY, mockget, - mockpost, - shapeshift, - rpc + mockpost }; diff --git a/js/src/3rdparty/shapeshift/rpc.spec.js b/js/src/3rdparty/shapeshift/rpc.spec.js index 582b77a53..d561fbe7d 100644 --- a/js/src/3rdparty/shapeshift/rpc.spec.js +++ b/js/src/3rdparty/shapeshift/rpc.spec.js @@ -16,12 +16,21 @@ const helpers = require('./helpers.spec.js'); -const APIKEY = helpers.APIKEY; +const ShapeShift = require('./'); +const initShapeshift = (ShapeShift.default || ShapeShift); + const mockget = helpers.mockget; const mockpost = helpers.mockpost; -const rpc = helpers.rpc; describe('shapeshift/rpc', () => { + let rpc; + let shapeshift; + + beforeEach(() => { + shapeshift = initShapeshift(helpers.APIKEY); + rpc = shapeshift.getRpc(); + }); + describe('GET', () => { const REPLY = { test: 'this is some result' }; @@ -29,7 +38,7 @@ describe('shapeshift/rpc', () => { let result; beforeEach(() => { - scope = mockget([{ path: 'test', reply: REPLY }]); + scope = mockget(shapeshift, [{ path: 'test', reply: REPLY }]); return rpc .get('test') @@ -54,7 +63,7 @@ describe('shapeshift/rpc', () => { let result; beforeEach(() => { - scope = mockpost([{ path: 'test', reply: REPLY }]); + scope = mockpost(shapeshift, [{ path: 'test', reply: REPLY }]); return rpc .post('test', { input: 'stuff' }) @@ -76,7 +85,7 @@ describe('shapeshift/rpc', () => { }); it('passes the apikey specified', () => { - expect(scope.body.test.apiKey).to.equal(APIKEY); + expect(scope.body.test.apiKey).to.equal(helpers.APIKEY); }); }); }); diff --git a/js/src/3rdparty/shapeshift/shapeshift.js b/js/src/3rdparty/shapeshift/shapeshift.js index 39b8365ec..c98ef3eca 100644 --- a/js/src/3rdparty/shapeshift/shapeshift.js +++ b/js/src/3rdparty/shapeshift/shapeshift.js @@ -15,8 +15,9 @@ // along with Parity. If not, see . export default function (rpc) { - let subscriptions = []; - let pollStatusIntervalId = null; + let _subscriptions = []; + let _pollStatusIntervalId = null; + let _subscriptionPromises = null; function getCoins () { return rpc.get('getcoins'); @@ -36,75 +37,93 @@ export default function (rpc) { function shift (toAddress, returnAddress, pair) { return rpc.post('shift', { - withdrawal: toAddress, - pair: pair, - returnAddress: returnAddress + pair, + returnAddress, + withdrawal: toAddress }); } function subscribe (depositAddress, callback) { - const idx = subscriptions.length; + if (!depositAddress || !callback) { + return; + } - subscriptions.push({ - depositAddress, + const index = _subscriptions.length; + + _subscriptions.push({ callback, - idx + depositAddress, + index }); - // Only poll if there are subscriptions... - if (!pollStatusIntervalId) { - pollStatusIntervalId = setInterval(_pollStatus, 2000); + if (_pollStatusIntervalId === null) { + _pollStatusIntervalId = setInterval(_pollStatus, 2000); } } function unsubscribe (depositAddress) { - const newSubscriptions = [] - .concat(subscriptions) - .filter((sub) => sub.depositAddress !== depositAddress); + _subscriptions = _subscriptions.filter((sub) => sub.depositAddress !== depositAddress); - subscriptions = newSubscriptions; - - if (subscriptions.length === 0) { - clearInterval(pollStatusIntervalId); - pollStatusIntervalId = null; + if (_subscriptions.length === 0) { + clearInterval(_pollStatusIntervalId); + _pollStatusIntervalId = null; } + + return true; } function _getSubscriptionStatus (subscription) { if (!subscription) { - return; + return Promise.resolve(); } - getStatus(subscription.depositAddress) + return getStatus(subscription.depositAddress) .then((result) => { switch (result.status) { case 'no_deposits': case 'received': subscription.callback(null, result); - return; + return true; case 'complete': subscription.callback(null, result); - subscriptions[subscription.idx] = null; - return; + unsubscribe(subscription.depositAddress); + return true; case 'failed': subscription.callback({ message: status.error, fatal: true }); - subscriptions[subscription.idx] = null; - return; + unsubscribe(subscription.depositAddress); + return true; } }) - .catch(subscription.callback); + .catch(() => { + return true; + }); } function _pollStatus () { - subscriptions.forEach(_getSubscriptionStatus); + _subscriptionPromises = Promise.all(_subscriptions.map(_getSubscriptionStatus)); + } + + function _getSubscriptions () { + return _subscriptions; + } + + function _getSubscriptionPromises () { + return _subscriptionPromises; + } + + function _isPolling () { + return _pollStatusIntervalId !== null; } return { + _getSubscriptions, + _getSubscriptionPromises, + _isPolling, getCoins, getMarketInfo, getRpc, diff --git a/js/src/3rdparty/shapeshift/shapeshift.spec.js b/js/src/3rdparty/shapeshift/shapeshift.spec.js index 9fe2ca1f0..1897178b3 100644 --- a/js/src/3rdparty/shapeshift/shapeshift.spec.js +++ b/js/src/3rdparty/shapeshift/shapeshift.spec.js @@ -14,13 +14,29 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +const sinon = require('sinon'); + +const ShapeShift = require('./'); +const initShapeshift = (ShapeShift.default || ShapeShift); + const helpers = require('./helpers.spec.js'); const mockget = helpers.mockget; const mockpost = helpers.mockpost; -const shapeshift = helpers.shapeshift; describe('shapeshift/calls', () => { + let clock; + let shapeshift; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + shapeshift = initShapeshift(helpers.APIKEY); + }); + + afterEach(() => { + clock.restore(); + }); + describe('getCoins', () => { const REPLY = { BTC: { @@ -39,8 +55,8 @@ describe('shapeshift/calls', () => { let scope; - before(() => { - scope = mockget([{ path: 'getcoins', reply: REPLY }]); + beforeEach(() => { + scope = mockget(shapeshift, [{ path: 'getcoins', reply: REPLY }]); return shapeshift.getCoins(); }); @@ -61,8 +77,8 @@ describe('shapeshift/calls', () => { let scope; - before(() => { - scope = mockget([{ path: 'marketinfo/btc_ltc', reply: REPLY }]); + beforeEach(() => { + scope = mockget(shapeshift, [{ path: 'marketinfo/btc_ltc', reply: REPLY }]); return shapeshift.getMarketInfo('btc_ltc'); }); @@ -80,8 +96,8 @@ describe('shapeshift/calls', () => { let scope; - before(() => { - scope = mockget([{ path: 'txStat/0x123', reply: REPLY }]); + beforeEach(() => { + scope = mockget(shapeshift, [{ path: 'txStat/0x123', reply: REPLY }]); return shapeshift.getStatus('0x123'); }); @@ -101,8 +117,8 @@ describe('shapeshift/calls', () => { let scope; - before(() => { - scope = mockpost([{ path: 'shift', reply: REPLY }]); + beforeEach(() => { + scope = mockpost(shapeshift, [{ path: 'shift', reply: REPLY }]); return shapeshift.shift('0x456', '1BTC', 'btc_eth'); }); @@ -125,4 +141,80 @@ describe('shapeshift/calls', () => { }); }); }); + + describe('subscriptions', () => { + const ADDRESS = '0123456789abcdef'; + const REPLY = { + status: 'complete', + address: ADDRESS + }; + + let callback; + + beforeEach(() => { + mockget(shapeshift, [{ path: `txStat/${ADDRESS}`, reply: REPLY }]); + callback = sinon.stub(); + shapeshift.subscribe(ADDRESS, callback); + }); + + describe('subscribe', () => { + it('adds the depositAddress to the list', () => { + const subscriptions = shapeshift._getSubscriptions(); + + expect(subscriptions.length).to.equal(1); + expect(subscriptions[0].depositAddress).to.equal(ADDRESS); + }); + + it('starts the polling timer', () => { + expect(shapeshift._isPolling()).to.be.true; + }); + + it('calls the callback once the timer has elapsed', () => { + clock.tick(2222); + + return shapeshift._getSubscriptionPromises().then(() => { + expect(callback).to.have.been.calledWith(null, REPLY); + }); + }); + + it('auto-unsubscribes on completed', () => { + clock.tick(2222); + + return shapeshift._getSubscriptionPromises().then(() => { + expect(shapeshift._getSubscriptions().length).to.equal(0); + }); + }); + }); + + describe('unsubscribe', () => { + it('unbsubscribes when requested', () => { + expect(shapeshift._getSubscriptions().length).to.equal(1); + shapeshift.unsubscribe(ADDRESS); + expect(shapeshift._getSubscriptions().length).to.equal(0); + }); + + it('clears the polling on no subscriptions', () => { + shapeshift.unsubscribe(ADDRESS); + expect(shapeshift._isPolling()).to.be.false; + }); + + it('handles unsubscribe of auto-unsubscribe', () => { + clock.tick(2222); + + return shapeshift._getSubscriptionPromises().then(() => { + expect(shapeshift.unsubscribe(ADDRESS)).to.be.true; + }); + }); + + it('handles unsubscribe when multiples listed', () => { + const ADDRESS2 = 'abcdef0123456789'; + + shapeshift.subscribe(ADDRESS2, sinon.stub()); + expect(shapeshift._getSubscriptions().length).to.equal(2); + expect(shapeshift._getSubscriptions()[0].depositAddress).to.equal(ADDRESS); + shapeshift.unsubscribe(ADDRESS); + expect(shapeshift._getSubscriptions()[0].depositAddress).to.equal(ADDRESS2); + }); + }); + }); }); From cc8e200ed50bc5b3f56d57d647b7182123aa70e2 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Wed, 4 Jan 2017 15:15:11 +0100 Subject: [PATCH 11/22] Connection UI cleanups & tests for prior PR (#4020) * Cleanups & tests for #3945 * Externalise icons as per PR comments --- js/src/ui/Icons/index.js | 10 +- js/src/views/Connection/connection.js | 54 +++---- js/src/views/Connection/connection.spec.js | 156 +++++++++++++++++++++ 3 files changed, 195 insertions(+), 25 deletions(-) create mode 100644 js/src/views/Connection/connection.spec.js diff --git a/js/src/ui/Icons/index.js b/js/src/ui/Icons/index.js index 8901d7dc7..4cf5a2d7d 100644 --- a/js/src/ui/Icons/index.js +++ b/js/src/ui/Icons/index.js @@ -18,7 +18,10 @@ import AddIcon from 'material-ui/svg-icons/content/add'; import CancelIcon from 'material-ui/svg-icons/content/clear'; import CheckIcon from 'material-ui/svg-icons/navigation/check'; import CloseIcon from 'material-ui/svg-icons/navigation/close'; +import CompareIcon from 'material-ui/svg-icons/action/compare-arrows'; +import ComputerIcon from 'material-ui/svg-icons/hardware/desktop-mac'; import ContractIcon from 'material-ui/svg-icons/action/code'; +import DashboardIcon from 'material-ui/svg-icons/action/dashboard'; import DoneIcon from 'material-ui/svg-icons/action/done-all'; import LockedIcon from 'material-ui/svg-icons/action/lock-outline'; import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward'; @@ -27,13 +30,17 @@ import SaveIcon from 'material-ui/svg-icons/content/save'; import SendIcon from 'material-ui/svg-icons/content/send'; import SnoozeIcon from 'material-ui/svg-icons/av/snooze'; import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye'; +import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock'; export { AddIcon, CancelIcon, CheckIcon, CloseIcon, + CompareIcon, + ComputerIcon, ContractIcon, + DashboardIcon, DoneIcon, LockedIcon, NextIcon, @@ -41,5 +48,6 @@ export { SaveIcon, SendIcon, SnoozeIcon, - VisibleIcon + VisibleIcon, + VpnIcon }; diff --git a/js/src/views/Connection/connection.js b/js/src/views/Connection/connection.js index 4f2ae7b1d..505840e1e 100644 --- a/js/src/views/Connection/connection.js +++ b/js/src/views/Connection/connection.js @@ -17,13 +17,9 @@ import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import ActionCompareArrows from 'material-ui/svg-icons/action/compare-arrows'; -import ActionDashboard from 'material-ui/svg-icons/action/dashboard'; -import HardwareDesktopMac from 'material-ui/svg-icons/hardware/desktop-mac'; -import NotificationVpnLock from 'material-ui/svg-icons/notification/vpn-lock'; import { Input } from '~/ui'; +import { CompareIcon, ComputerIcon, DashboardIcon, VpnIcon } from '~/ui/Icons'; import styles from './connection.css'; @@ -51,13 +47,6 @@ class Connection extends Component { return null; } - const typeIcon = needsToken - ? - : ; - const description = needsToken - ? this.renderSigner() - : this.renderPing(); - return (
@@ -65,16 +54,24 @@ class Connection extends Component {
- +
- +
- { typeIcon } + { + needsToken + ? + : + }
- { description } + { + needsToken + ? this.renderSigner() + : this.renderPing() + }
@@ -144,10 +141,19 @@ class Connection extends Component { ); } - onChangeToken = (event, _token) => { + validateToken = (_token) => { const token = _token.trim(); const validToken = /^[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}$/.test(token); + return { + token, + validToken + }; + } + + onChangeToken = (event, _token) => { + const { token, validToken } = this.validateToken(_token || event.target.value); + this.setState({ token, validToken }, () => { validToken && this.setToken(); }); @@ -159,7 +165,7 @@ class Connection extends Component { this.setState({ loading: true }); - api + return api .updateToken(token, 0) .then((isValid) => { this.setState({ @@ -173,14 +179,14 @@ class Connection extends Component { function mapStateToProps (state) { const { isConnected, isConnecting, needsToken } = state.nodeStatus; - return { isConnected, isConnecting, needsToken }; -} - -function mapDispatchToProps (dispatch) { - return bindActionCreators({}, dispatch); + return { + isConnected, + isConnecting, + needsToken + }; } export default connect( mapStateToProps, - mapDispatchToProps + null )(Connection); diff --git a/js/src/views/Connection/connection.spec.js b/js/src/views/Connection/connection.spec.js new file mode 100644 index 000000000..20c41b3e4 --- /dev/null +++ b/js/src/views/Connection/connection.spec.js @@ -0,0 +1,156 @@ +// Copyright 2015, 2016 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 { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; + +import Connection from './'; + +let api; +let component; +let instance; + +function createApi () { + return { + updateToken: sinon.stub().resolves() + }; +} + +function createRedux (isConnected = true, isConnecting = false, needsToken = false) { + return { + dispatch: sinon.stub(), + subscribe: sinon.stub(), + getState: () => { + return { + nodeStatus: { + isConnected, + isConnecting, + needsToken + } + }; + } + }; +} + +function render (store) { + api = createApi(); + component = shallow( + , + { context: { store: store || createRedux() } } + ).find('Connection').shallow({ context: { api } }); + instance = component.instance(); + + return component; +} + +describe('views/Connection', () => { + it('renders defaults', () => { + expect(render()).to.be.ok; + }); + + it('does not render when connected', () => { + expect(render(createRedux(true)).find('div')).to.have.length(0); + }); + + describe('renderPing', () => { + it('renders the connecting to node message', () => { + render(); + const ping = shallow(instance.renderPing()); + + expect(ping.find('FormattedMessage').props().id).to.equal('connection.connectingNode'); + }); + }); + + describe('renderSigner', () => { + it('renders the connecting to api message when isConnecting === true', () => { + render(createRedux(false, true)); + const signer = shallow(instance.renderSigner()); + + expect(signer.find('FormattedMessage').props().id).to.equal('connection.connectingAPI'); + }); + + it('renders token input when needsToken == true & isConnecting === false', () => { + render(createRedux(false, false, true)); + const signer = shallow(instance.renderSigner()); + + expect(signer.find('FormattedMessage').first().props().id).to.equal('connection.noConnection'); + }); + }); + + describe('validateToken', () => { + beforeEach(() => { + render(); + }); + + it('trims whitespace from passed tokens', () => { + expect(instance.validateToken(' \t test ing\t ').token).to.equal('test ing'); + }); + + it('validates 4-4-4-4 format', () => { + expect(instance.validateToken('1234-5678-90ab-cdef').validToken).to.be.true; + }); + + it('validates 4-4-4-4 format (with trimmable whitespace)', () => { + expect(instance.validateToken(' \t 1234-5678-90ab-cdef \t ').validToken).to.be.true; + }); + + it('validates 4444 format', () => { + expect(instance.validateToken('1234567890abcdef').validToken).to.be.true; + }); + + it('validates 4444 format (with trimmable whitespace)', () => { + expect(instance.validateToken(' \t 1234567890abcdef \t ').validToken).to.be.true; + }); + }); + + describe('onChangeToken', () => { + beforeEach(() => { + render(); + sinon.spy(instance, 'setToken'); + sinon.spy(instance, 'validateToken'); + }); + + afterEach(() => { + instance.setToken.restore(); + instance.validateToken.restore(); + }); + + it('validates tokens passed', () => { + instance.onChangeToken({ target: { value: 'testing' } }); + expect(instance.validateToken).to.have.been.calledWith('testing'); + }); + + it('sets the token on the api when valid', () => { + instance.onChangeToken({ target: { value: '1234-5678-90ab-cdef' } }); + expect(instance.setToken).to.have.been.called; + }); + }); + + describe('setToken', () => { + beforeEach(() => { + render(); + }); + + it('calls the api.updateToken', () => { + component.setState({ token: 'testing' }); + + return instance.setToken().then(() => { + expect(api.updateToken).to.have.been.calledWith('testing'); + }); + }); + }); +}); From 71e7a429d76948dedb539b52e9ffda9c023829cb Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Wed, 4 Jan 2017 15:15:25 +0100 Subject: [PATCH 12/22] Only fetch App when necessary (#4023) * Only fetch App when necessary. Show loadings + 404 #3914 * PR Grumble --- js/src/views/Dapp/dapp.css | 20 +++++++++++ js/src/views/Dapp/dapp.js | 59 +++++++++++++++++++++++++++++--- js/src/views/Dapps/dapps.js | 4 +++ js/src/views/Dapps/dappsStore.js | 36 +++++++++++++++---- 4 files changed, 109 insertions(+), 10 deletions(-) diff --git a/js/src/views/Dapp/dapp.css b/js/src/views/Dapp/dapp.css index b0f8f06e6..70b39ce08 100644 --- a/js/src/views/Dapp/dapp.css +++ b/js/src/views/Dapp/dapp.css @@ -20,3 +20,23 @@ height: 100%; width: 100%; } + +.full { + width: 100vw; + height: 100vh; + margin: 0; + padding: 0; + background: white; + font-family: 'Roboto', sans-serif; + font-size: 16px; + font-weight: 300; + + .text { + text-align: center; + padding: 5em; + font-size: 2em; + color: #999; + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/js/src/views/Dapp/dapp.js b/js/src/views/Dapp/dapp.js index 87245ca72..7f1416392 100644 --- a/js/src/views/Dapp/dapp.js +++ b/js/src/views/Dapp/dapp.js @@ -16,6 +16,7 @@ import React, { Component, PropTypes } from 'react'; import { observer } from 'mobx-react'; +import { FormattedMessage } from 'react-intl'; import DappsStore from '../Dapps/dappsStore'; @@ -25,21 +26,71 @@ import styles from './dapp.css'; export default class Dapp extends Component { static contextTypes = { api: PropTypes.object.isRequired - } + }; static propTypes = { params: PropTypes.object }; + state = { + app: null, + loading: true + }; + store = DappsStore.get(this.context.api); + componentWillMount () { + const { id } = this.props.params; + this.loadApp(id); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.params.id !== this.props.params.id) { + this.loadApp(nextProps.params.id); + } + } + + loadApp (id) { + this.setState({ loading: true }); + + this.store + .loadApp(id) + .then((app) => { + this.setState({ loading: false, app }); + }) + .catch(() => { + this.setState({ loading: false }); + }); + } + render () { const { dappsUrl } = this.context.api; - const { id } = this.props.params; - const app = this.store.apps.find((app) => app.id === id); + const { app, loading } = this.state; + + if (loading) { + return ( +
+
+ +
+
+ ); + } if (!app) { - return null; + return ( +
+
+ +
+
+ ); } let src = null; diff --git a/js/src/views/Dapps/dapps.js b/js/src/views/Dapps/dapps.js index e800263e4..fa7c44878 100644 --- a/js/src/views/Dapps/dapps.js +++ b/js/src/views/Dapps/dapps.js @@ -45,6 +45,10 @@ class Dapps extends Component { store = DappsStore.get(this.context.api); permissionStore = new PermissionStore(this.context.api); + componentWillMount () { + this.store.loadAllApps(); + } + render () { let externalOverlay = null; if (this.store.externalOverlayVisible) { diff --git a/js/src/views/Dapps/dappsStore.js b/js/src/views/Dapps/dappsStore.js index 42342aff3..8cca4d3f7 100644 --- a/js/src/views/Dapps/dappsStore.js +++ b/js/src/views/Dapps/dappsStore.js @@ -48,17 +48,43 @@ export default class DappsStore { this.readDisplayApps(); this.loadExternalOverlay(); - this.loadApps(); this.subscribeToChanges(); } - loadApps () { + /** + * Try to find the app from the local (local or builtin) + * apps, else fetch from the node + */ + loadApp (id) { const { dappReg } = Contracts.get(); - Promise + return this + .loadLocalApps() + .then(() => { + const app = this.apps.find((app) => app.id === id); + + if (app) { + return app; + } + + return this.fetchRegistryApp(dappReg, id, true); + }); + } + + loadLocalApps () { + return Promise .all([ this.fetchBuiltinApps().then((apps) => this.addApps(apps)), - this.fetchLocalApps().then((apps) => this.addApps(apps)), + this.fetchLocalApps().then((apps) => this.addApps(apps)) + ]); + } + + loadAllApps () { + const { dappReg } = Contracts.get(); + + return Promise + .all([ + this.loadLocalApps(), this.fetchRegistryApps(dappReg).then((apps) => this.addApps(apps)) ]) .then(this.writeDisplayApps); @@ -67,8 +93,6 @@ export default class DappsStore { static get (api) { if (!instance) { instance = new DappsStore(api); - } else { - instance.loadApps(); } return instance; From 839ee9afd7a4ee95117eddb4470fc9ec4bc9f9a6 Mon Sep 17 00:00:00 2001 From: Jannis Redmann Date: Wed, 4 Jan 2017 15:15:36 +0100 Subject: [PATCH 13/22] address selector: support reverse lookup (#4033) * address selector: simplify lookup * address selector: support reverse lookup --- js/src/ui/Form/AddressSelect/addressSelect.js | 3 +- .../Form/AddressSelect/addressSelectStore.js | 128 +++++++++++------- 2 files changed, 83 insertions(+), 48 deletions(-) diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index ca4e1f524..ba48b6489 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -215,8 +215,9 @@ class AddressSelect extends Component { } const { address, addressError } = validateAddress(inputValue); + const { registryValues } = this.store; - if (addressError) { + if (addressError || registryValues.length > 0) { return null; } diff --git a/js/src/ui/Form/AddressSelect/addressSelectStore.js b/js/src/ui/Form/AddressSelect/addressSelectStore.js index b6827b9cc..26f9fe80e 100644 --- a/js/src/ui/Form/AddressSelect/addressSelectStore.js +++ b/js/src/ui/Form/AddressSelect/addressSelectStore.js @@ -22,6 +22,8 @@ import { FormattedMessage } from 'react-intl'; import Contracts from '~/contracts'; import { sha3 } from '~/api/util/sha3'; +const ZERO = /^(0x)?0*$/; + export default class AddressSelectStore { @observable values = []; @@ -38,41 +40,75 @@ export default class AddressSelectStore { registry .getContract('emailverification') .then((emailVerification) => { - this.regLookups.push({ - lookup: (value) => { - return emailVerification - .instance - .reverse.call({}, [ sha3(value) ]); - }, - describe: (value) => ( - - ) + this.regLookups.push((email) => { + return emailVerification + .instance + .reverse + .call({}, [ sha3(email) ]) + .then((address) => { + return { + address, + description: ( + + ) + }; + }); }); }); registry .getInstance() .then((registryInstance) => { - this.regLookups.push({ - lookup: (value) => { - return registryInstance - .getAddress.call({}, [ sha3(value), 'A' ]); - }, - describe: (value) => ( - - ) + this.regLookups.push((name) => { + return registryInstance + .getAddress + .call({}, [ sha3(name), 'A' ]) + .then((address) => { + return { + address, + name, + description: ( + + ) + }; + }); + }); + + this.regLookups.push((address) => { + return registryInstance + .reverse + .call({}, [ address ]) + .then((name) => { + if (!name) { + return null; + } + + return { + address, + name, + description: ( + + ) + }; + }); }); }); } @@ -149,32 +185,30 @@ export default class AddressSelectStore { // Registries Lookup this.registryValues = []; - const lookups = this.regLookups.map((regLookup) => regLookup.lookup(value)); + const lookups = this.regLookups.map((regLookup) => regLookup(value)); Promise .all(lookups) .then((results) => { return results - .map((result, index) => { - if (/^(0x)?0*$/.test(result)) { - return; - } - - const lowercaseResult = result.toLowerCase(); + .filter((result) => result && !ZERO.test(result.address)); + }) + .then((results) => { + this.registryValues = results + .map((result) => { + const lowercaseAddress = result.address.toLowerCase(); const account = flatMap(this.initValues, (cat) => cat.values) - .find((account) => account.address.toLowerCase() === lowercaseResult); + .find((account) => account.address.toLowerCase() === lowercaseAddress); - return { - description: this.regLookups[index].describe(value), - address: result, - name: account && account.name || value - }; - }) - .filter((data) => data); - }) - .then((registryValues) => { - this.registryValues = registryValues; + if (account && account.name) { + result.name = account.name; + } else if (!result.name) { + result.name = value; + } + + return result; + }); }); } From 6861230bee4f827a89367f58bfa4276000722f4e Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Wed, 4 Jan 2017 14:24:09 +0000 Subject: [PATCH 14/22] [ci skip] js-precompiled 20170104-142103 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d20d65a90..3bb342ca6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1500,7 +1500,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#ebea2bf78e076916b51b04d8b24187a6a85ae440" +source = "git+https://github.com/ethcore/js-precompiled.git#567fe36d9db1d120ca7f6fb58f6f66649a3d7f34" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index 8672578c7..8e45c9373 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.2.165", + "version": "0.2.166", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", From fbb52ac58bc65939e2d4c4c496e1e36e0fd6efe5 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Wed, 4 Jan 2017 16:25:39 +0100 Subject: [PATCH 15/22] Fix wallet in main net (#4038) --- js/src/views/Wallet/wallet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/views/Wallet/wallet.js b/js/src/views/Wallet/wallet.js index 5418448b4..7a895d6dc 100644 --- a/js/src/views/Wallet/wallet.js +++ b/js/src/views/Wallet/wallet.js @@ -180,7 +180,7 @@ class Wallet extends Component { const { address, isTest, wallet } = this.props; const { owners, require, confirmations, transactions } = wallet; - if (!isTest || !owners || !require) { + if (!owners || !require) { return (
From 7ebf8be12e4329a045206933bc9f724d16e5b948 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Wed, 4 Jan 2017 15:34:36 +0000 Subject: [PATCH 16/22] [ci skip] js-precompiled 20170104-153136 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3bb342ca6..2fee4b290 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1500,7 +1500,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#567fe36d9db1d120ca7f6fb58f6f66649a3d7f34" +source = "git+https://github.com/ethcore/js-precompiled.git#9fc9d894356b135c66cc99bf66ec2a713b5f0a8c" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index 8e45c9373..08af16a0a 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.2.166", + "version": "0.2.167", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", From a4b4263580bcc2d8873a3b3c3f4ec84b689a8b8c Mon Sep 17 00:00:00 2001 From: maciejhirsz Date: Wed, 4 Jan 2017 16:51:27 +0100 Subject: [PATCH 17/22] Adaptive hints --- parity/params.rs | 29 ++++++++++++++++++++++++++++- parity/run.rs | 16 +++++++++------- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/parity/params.rs b/parity/params.rs index 9399b33e4..93d109979 100644 --- a/parity/params.rs +++ b/parity/params.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -use std::{str, fs}; +use std::{str, fs, fmt}; use std::time::Duration; use util::{Address, U256, version_data}; use util::journaldb::Algorithm; @@ -60,6 +60,21 @@ impl str::FromStr for SpecType { } } +impl fmt::Display for SpecType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match *self { + SpecType::Mainnet => "homestead", + SpecType::Morden => "morden", + SpecType::Ropsten => "ropsten", + SpecType::Olympic => "olympic", + SpecType::Classic => "classic", + SpecType::Expanse => "expanse", + SpecType::Dev => "dev", + SpecType::Custom(ref custom) => custom, + }) + } +} + impl SpecType { pub fn spec(&self) -> Result { match *self { @@ -305,6 +320,18 @@ mod tests { assert_eq!(SpecType::Mainnet, SpecType::default()); } + #[test] + fn test_spec_type_display() { + assert_eq!(format!("{}", SpecType::Mainnet), "homestead"); + assert_eq!(format!("{}", SpecType::Ropsten), "ropsten"); + assert_eq!(format!("{}", SpecType::Morden), "morden"); + assert_eq!(format!("{}", SpecType::Olympic), "olympic"); + assert_eq!(format!("{}", SpecType::Classic), "classic"); + assert_eq!(format!("{}", SpecType::Expanse), "expanse"); + assert_eq!(format!("{}", SpecType::Dev), "dev"); + assert_eq!(format!("{}", SpecType::Custom("foo/bar".into())), "foo/bar"); + } + #[test] fn test_pruning_parsing() { assert_eq!(Pruning::Auto, "auto".parse().unwrap()); diff --git a/parity/run.rs b/parity/run.rs index de9d7a639..9c31561eb 100644 --- a/parity/run.rs +++ b/parity/run.rs @@ -60,9 +60,6 @@ const SNAPSHOT_PERIOD: u64 = 10000; // how many blocks to wait before starting a periodic snapshot. const SNAPSHOT_HISTORY: u64 = 100; -// Pops along with error message when an account address is not found. -const CREATE_ACCOUNT_HINT: &'static str = "Please run `parity account new [-d current-d --chain current-chain --keys-path current-keys-path]`."; - // Pops along with error messages when a password is missing or invalid. const VERIFY_PASSWORD_HINT: &'static str = "Make sure valid password is present in files passed using `--password` or in the configuration file."; @@ -223,7 +220,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R let passwords = passwords_from_files(&cmd.acc_conf.password_files)?; // prepare account provider - let account_provider = Arc::new(prepare_account_provider(&cmd.dirs, &spec.data_dir, cmd.acc_conf, &passwords)?); + let account_provider = Arc::new(prepare_account_provider(&cmd.spec, &cmd.dirs, &spec.data_dir, cmd.acc_conf, &passwords)?); // let the Engine access the accounts spec.engine.register_account_provider(account_provider.clone()); @@ -240,7 +237,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R if engine_signer != Default::default() { // Check if engine signer exists if !account_provider.has_account(engine_signer).unwrap_or(false) { - return Err(format!("Consensus signer account not found for the current chain. {}", CREATE_ACCOUNT_HINT)); + return Err(format!("Consensus signer account not found for the current chain. {}", build_create_account_hint(&cmd.spec, &cmd.dirs.keys))); } // Check if any passwords have been read from the password file(s) @@ -489,7 +486,7 @@ fn daemonize(_pid_file: String) -> Result<(), String> { Err("daemon is no supported on windows".into()) } -fn prepare_account_provider(dirs: &Directories, data_dir: &str, cfg: AccountsConfig, passwords: &[String]) -> Result { +fn prepare_account_provider(spec: &SpecType, dirs: &Directories, data_dir: &str, cfg: AccountsConfig, passwords: &[String]) -> Result { use ethcore::ethstore::EthStore; use ethcore::ethstore::dir::DiskDirectory; @@ -503,7 +500,7 @@ fn prepare_account_provider(dirs: &Directories, data_dir: &str, cfg: AccountsCon for a in cfg.unlocked_accounts { // Check if the account exists if !account_provider.has_account(a).unwrap_or(false) { - return Err(format!("Account {} not found for the current chain. {}", a, CREATE_ACCOUNT_HINT)); + return Err(format!("Account {} not found for the current chain. {}", a, build_create_account_hint(spec, &dirs.keys))); } // Check if any passwords have been read from the password file(s) @@ -519,6 +516,11 @@ fn prepare_account_provider(dirs: &Directories, data_dir: &str, cfg: AccountsCon Ok(account_provider) } +// Construct an error `String` with an adaptive hint on how to create an account. +fn build_create_account_hint(spec: &SpecType, keys: &str) -> String { + format!("You can create an account via RPC, UI or `parity account new --chain {} --keys-path {}`.", spec, keys) +} + fn wait_for_exit( panic_handler: Arc, _http_server: Option, From 602a4429cc1d0eaf11e4b9eac61e7e7d38dc7e1e Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Thu, 5 Jan 2017 12:06:35 +0100 Subject: [PATCH 18/22] Account view updates (#4008) * Fix null account render issue, add tests * Add tests for #3999 fix (merged in #4000) * Only include sinon-as-promised globally for mocha * Move transactions state into tested store * Add esjify for mocha + ejs (cherry-picked) * Extract store state into store, test it * Use address (as per PR comments) * Fix failing test after master merge --- js/package.json | 5 +- js/src/3rdparty/etherscan/account.js | 6 +- js/src/3rdparty/etherscan/helpers.spec.js | 38 +++ js/src/api/subscriptions/eth.spec.js | 1 - js/src/api/subscriptions/personal.spec.js | 1 - js/src/ui/Actionbar/actionbar.js | 3 +- js/src/ui/Certifications/certifications.js | 6 +- js/src/ui/Icons/index.js | 8 +- js/src/views/Account/Header/header.js | 73 +++-- js/src/views/Account/Header/header.spec.js | 156 ++++++++++ js/src/views/Account/Transactions/store.js | 118 ++++++++ .../views/Account/Transactions/store.spec.js | 193 ++++++++++++ .../Account/Transactions/transactions.js | 107 ++----- .../Account/Transactions/transactions.spec.js | 55 ++++ .../Account/Transactions/transactions.test.js | 31 ++ js/src/views/Account/account.js | 275 +++++++----------- js/src/views/Account/account.spec.js | 226 ++++++++++++++ js/src/views/Account/account.test.js | 52 ++++ js/src/views/Account/store.js | 50 ++++ js/src/views/Account/store.spec.js | 84 ++++++ js/src/views/Accounts/Summary/summary.js | 2 +- js/test/mocha.config.js | 1 + 22 files changed, 1210 insertions(+), 281 deletions(-) create mode 100644 js/src/3rdparty/etherscan/helpers.spec.js create mode 100644 js/src/views/Account/Header/header.spec.js create mode 100644 js/src/views/Account/Transactions/store.js create mode 100644 js/src/views/Account/Transactions/store.spec.js create mode 100644 js/src/views/Account/Transactions/transactions.spec.js create mode 100644 js/src/views/Account/Transactions/transactions.test.js create mode 100644 js/src/views/Account/account.spec.js create mode 100644 js/src/views/Account/account.test.js create mode 100644 js/src/views/Account/store.js create mode 100644 js/src/views/Account/store.spec.js diff --git a/js/package.json b/js/package.json index 08af16a0a..29bb70791 100644 --- a/js/package.json +++ b/js/package.json @@ -43,8 +43,8 @@ "lint:css": "stylelint ./src/**/*.css", "lint:js": "eslint --ignore-path .gitignore ./src/", "lint:js:cached": "eslint --cache --ignore-path .gitignore ./src/", - "test": "NODE_ENV=test mocha 'src/**/*.spec.js'", - "test:coverage": "NODE_ENV=test istanbul cover _mocha -- 'src/**/*.spec.js'", + "test": "NODE_ENV=test mocha --compilers ejs:ejsify 'src/**/*.spec.js'", + "test:coverage": "NODE_ENV=test istanbul cover _mocha -- --compilers ejs:ejsify 'src/**/*.spec.js'", "test:e2e": "NODE_ENV=test mocha 'src/**/*.e2e.js'", "test:npm": "(cd .npmjs && npm i) && node test/npmParity && (rm -rf .npmjs/node_modules)", "prepush": "npm run lint:cached" @@ -80,6 +80,7 @@ "coveralls": "2.11.15", "css-loader": "0.26.1", "ejs-loader": "0.3.0", + "ejsify": "1.0.0", "enzyme": "2.7.0", "eslint": "3.11.1", "eslint-config-semistandard": "7.0.0", diff --git a/js/src/3rdparty/etherscan/account.js b/js/src/3rdparty/etherscan/account.js index 7b8c431a0..52a08ef4b 100644 --- a/js/src/3rdparty/etherscan/account.js +++ b/js/src/3rdparty/etherscan/account.js @@ -49,17 +49,17 @@ function transactions (address, page, test = false) { // page offset from 0 return _call('txlist', { address: address, - page: (page || 0) + 1, offset: PAGE_SIZE, + page: (page || 0) + 1, sort: 'desc' }, test).then((transactions) => { return transactions.map((tx) => { return { + blockNumber: new BigNumber(tx.blockNumber || 0), from: util.toChecksumAddress(tx.from), - to: util.toChecksumAddress(tx.to), hash: tx.hash, - blockNumber: new BigNumber(tx.blockNumber), timeStamp: tx.timeStamp, + to: util.toChecksumAddress(tx.to), value: tx.value }; }); diff --git a/js/src/3rdparty/etherscan/helpers.spec.js b/js/src/3rdparty/etherscan/helpers.spec.js new file mode 100644 index 000000000..508a7b47a --- /dev/null +++ b/js/src/3rdparty/etherscan/helpers.spec.js @@ -0,0 +1,38 @@ +// Copyright 2015, 2016 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 nock from 'nock'; +import { stringify } from 'qs'; + +import { url } from './links'; + +function mockget (requests, test) { + let scope = nock(url(test)); + + requests.forEach((request) => { + scope = scope + .get(`/api?${stringify(request.query)}`) + .reply(request.code || 200, () => { + return { result: request.reply }; + }); + }); + + return scope; +} + +export { + mockget +}; diff --git a/js/src/api/subscriptions/eth.spec.js b/js/src/api/subscriptions/eth.spec.js index 87cc76d03..680ff881e 100644 --- a/js/src/api/subscriptions/eth.spec.js +++ b/js/src/api/subscriptions/eth.spec.js @@ -16,7 +16,6 @@ import BigNumber from 'bignumber.js'; import sinon from 'sinon'; -import 'sinon-as-promised'; import Eth from './eth'; diff --git a/js/src/api/subscriptions/personal.spec.js b/js/src/api/subscriptions/personal.spec.js index b00354f64..2359192f0 100644 --- a/js/src/api/subscriptions/personal.spec.js +++ b/js/src/api/subscriptions/personal.spec.js @@ -15,7 +15,6 @@ // along with Parity. If not, see . import sinon from 'sinon'; -import 'sinon-as-promised'; import Personal from './personal'; diff --git a/js/src/ui/Actionbar/actionbar.js b/js/src/ui/Actionbar/actionbar.js index 0141016ab..49cc77df1 100644 --- a/js/src/ui/Actionbar/actionbar.js +++ b/js/src/ui/Actionbar/actionbar.js @@ -50,8 +50,7 @@ export default class Actionbar extends Component { } return ( - + { buttons } ); diff --git a/js/src/ui/Certifications/certifications.js b/js/src/ui/Certifications/certifications.js index bafd06f35..5604ab90a 100644 --- a/js/src/ui/Certifications/certifications.js +++ b/js/src/ui/Certifications/certifications.js @@ -25,7 +25,7 @@ import styles from './certifications.css'; class Certifications extends Component { static propTypes = { - account: PropTypes.string.isRequired, + address: PropTypes.string.isRequired, certifications: PropTypes.array.isRequired, dappsUrl: PropTypes.string.isRequired } @@ -60,10 +60,10 @@ class Certifications extends Component { } function mapStateToProps (_, initProps) { - const { account } = initProps; + const { address } = initProps; return (state) => { - const certifications = state.certifications[account] || []; + const certifications = state.certifications[address] || []; const dappsUrl = state.api.dappsUrl; return { certifications, dappsUrl }; diff --git a/js/src/ui/Icons/index.js b/js/src/ui/Icons/index.js index 4cf5a2d7d..1e0f93809 100644 --- a/js/src/ui/Icons/index.js +++ b/js/src/ui/Icons/index.js @@ -22,13 +22,16 @@ import CompareIcon from 'material-ui/svg-icons/action/compare-arrows'; import ComputerIcon from 'material-ui/svg-icons/hardware/desktop-mac'; import ContractIcon from 'material-ui/svg-icons/action/code'; import DashboardIcon from 'material-ui/svg-icons/action/dashboard'; +import DeleteIcon from 'material-ui/svg-icons/action/delete'; import DoneIcon from 'material-ui/svg-icons/action/done-all'; -import LockedIcon from 'material-ui/svg-icons/action/lock-outline'; +import EditIcon from 'material-ui/svg-icons/content/create'; +import LockedIcon from 'material-ui/svg-icons/action/lock'; import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward'; import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back'; import SaveIcon from 'material-ui/svg-icons/content/save'; import SendIcon from 'material-ui/svg-icons/content/send'; import SnoozeIcon from 'material-ui/svg-icons/av/snooze'; +import VerifyIcon from 'material-ui/svg-icons/action/verified-user'; import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye'; import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock'; @@ -41,13 +44,16 @@ export { ComputerIcon, ContractIcon, DashboardIcon, + DeleteIcon, DoneIcon, + EditIcon, LockedIcon, NextIcon, PrevIcon, SaveIcon, SendIcon, SnoozeIcon, + VerifyIcon, VisibleIcon, VpnIcon }; diff --git a/js/src/views/Account/Header/header.js b/js/src/views/Account/Header/header.js index 6e508d05e..f5694177a 100644 --- a/js/src/views/Account/Header/header.js +++ b/js/src/views/Account/Header/header.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags } from '~/ui'; import CopyToClipboard from '~/ui/CopyToClipboard'; @@ -26,50 +27,45 @@ export default class Header extends Component { static propTypes = { account: PropTypes.object, balance: PropTypes.object, - className: PropTypes.string, children: PropTypes.node, - isContract: PropTypes.bool, - hideName: PropTypes.bool + className: PropTypes.string, + hideName: PropTypes.bool, + isContract: PropTypes.bool }; static defaultProps = { - className: '', children: null, - isContract: false, - hideName: false + className: '', + hideName: false, + isContract: false }; render () { - const { account, balance, className, children, hideName } = this.props; - const { address, meta, uuid } = account; + const { account, balance, children, className, hideName } = this.props; + if (!account) { return null; } - const uuidText = !uuid - ? null - :
uuid: { uuid }
; + const { address } = account; + const meta = account.meta || {}; return (
- +
- { this.renderName(address) } - + { this.renderName() }
{ address }
- - { uuidText } + { this.renderUuid() }
{ meta.description }
{ this.renderTxCount() }
-
@@ -77,9 +73,7 @@ export default class Header extends Component { - +
{ children } @@ -87,15 +81,22 @@ export default class Header extends Component { ); } - renderName (address) { + renderName () { const { hideName } = this.props; if (hideName) { return null; } + const { address } = this.props.account; + return ( - } /> + + } /> ); } @@ -114,7 +115,31 @@ export default class Header extends Component { return (
- { txCount.toFormat() } outgoing transactions + +
+ ); + } + + renderUuid () { + const { uuid } = this.props.account; + + if (!uuid) { + return null; + } + + return ( +
+
); } diff --git a/js/src/views/Account/Header/header.spec.js b/js/src/views/Account/Header/header.spec.js new file mode 100644 index 000000000..5ae5104d2 --- /dev/null +++ b/js/src/views/Account/Header/header.spec.js @@ -0,0 +1,156 @@ +// Copyright 2015, 2016 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 BigNumber from 'bignumber.js'; +import { shallow } from 'enzyme'; +import React from 'react'; + +import Header from './'; + +const ACCOUNT = { + address: '0x0123456789012345678901234567890123456789', + meta: { + description: 'the description', + tags: ['taga', 'tagb'] + }, + uuid: '0xabcdef' +}; + +let component; +let instance; + +function render (props = {}) { + if (props && !props.account) { + props.account = ACCOUNT; + } + + component = shallow( +
+ ); + instance = component.instance(); + + return component; +} + +describe('views/Account/Header', () => { + describe('rendering', () => { + it('renders defaults', () => { + expect(render()).to.be.ok; + }); + + it('renders null with no account', () => { + expect(render(null).find('div')).to.have.length(0); + }); + + it('renders when no account meta', () => { + expect(render({ account: { address: ACCOUNT.address } })).to.be.ok; + }); + + it('renders when no account description', () => { + expect(render({ account: { address: ACCOUNT.address, meta: { tags: [] } } })).to.be.ok; + }); + + it('renders when no account tags', () => { + expect(render({ account: { address: ACCOUNT.address, meta: { description: 'something' } } })).to.be.ok; + }); + + describe('sections', () => { + it('renders the Balance', () => { + render({ balance: { balance: 'testing' } }); + const balance = component.find('Connect(Balance)'); + + expect(balance).to.have.length(1); + expect(balance.props().account).to.deep.equal(ACCOUNT); + expect(balance.props().balance).to.deep.equal({ balance: 'testing' }); + }); + + it('renders the Certifications', () => { + render(); + const certs = component.find('Connect(Certifications)'); + + expect(certs).to.have.length(1); + expect(certs.props().address).to.deep.equal(ACCOUNT.address); + }); + + it('renders the IdentityIcon', () => { + render(); + const icon = component.find('Connect(IdentityIcon)'); + + expect(icon).to.have.length(1); + expect(icon.props().address).to.equal(ACCOUNT.address); + }); + + it('renders the Tags', () => { + render(); + const tags = component.find('Tags'); + + expect(tags).to.have.length(1); + expect(tags.props().tags).to.deep.equal(ACCOUNT.meta.tags); + }); + }); + }); + + describe('renderName', () => { + it('renders null with hideName', () => { + render({ hideName: true }); + expect(instance.renderName()).to.be.null; + }); + + it('renders the name', () => { + render(); + expect(instance.renderName()).not.to.be.null; + }); + + it('renders when no address specified', () => { + render({ account: {} }); + expect(instance.renderName()).to.be.ok; + }); + }); + + describe('renderTxCount', () => { + it('renders null when contract', () => { + render({ balance: { txCount: new BigNumber(1) }, isContract: true }); + expect(instance.renderTxCount()).to.be.null; + }); + + it('renders null when no balance', () => { + render({ balance: null, isContract: false }); + expect(instance.renderTxCount()).to.be.null; + }); + + it('renders null when txCount is null', () => { + render({ balance: { txCount: null }, isContract: false }); + expect(instance.renderTxCount()).to.be.null; + }); + + it('renders the tx count', () => { + render({ balance: { txCount: new BigNumber(1) }, isContract: false }); + expect(instance.renderTxCount()).not.to.be.null; + }); + }); + + describe('renderUuid', () => { + it('renders null with no uuid', () => { + render({ account: Object.assign({}, ACCOUNT, { uuid: null }) }); + expect(instance.renderUuid()).to.be.null; + }); + + it('renders the uuid', () => { + render(); + expect(instance.renderUuid()).not.to.be.null; + }); + }); +}); diff --git a/js/src/views/Account/Transactions/store.js b/js/src/views/Account/Transactions/store.js new file mode 100644 index 000000000..d59595c44 --- /dev/null +++ b/js/src/views/Account/Transactions/store.js @@ -0,0 +1,118 @@ +// Copyright 2015, 2016 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, transaction } from 'mobx'; + +import etherscan from '~/3rdparty/etherscan'; + +export default class Store { + @observable address = null; + @observable isLoading = false; + @observable isTest = undefined; + @observable isTracing = false; + @observable txHashes = []; + + constructor (api) { + this._api = api; + } + + @action setHashes = (transactions) => { + transaction(() => { + this.setLoading(false); + this.txHashes = transactions.map((transaction) => transaction.hash); + }); + } + + @action setAddress = (address) => { + this.address = address; + } + + @action setLoading = (isLoading) => { + this.isLoading = isLoading; + } + + @action setTest = (isTest) => { + this.isTest = isTest; + } + + @action setTracing = (isTracing) => { + this.isTracing = isTracing; + } + + @action updateProps = (props) => { + transaction(() => { + this.setAddress(props.address); + this.setTest(props.isTest); + + // TODO: When tracing is enabled again, adjust to actually set + this.setTracing(false && props.traceMode); + }); + + return this.getTransactions(); + } + + getTransactions () { + if (this.isTest === undefined) { + return Promise.resolve(); + } + + this.setLoading(true); + + // TODO: When supporting other chains (eg. ETC). call to be made to other endpoints + return ( + this.isTracing + ? this.fetchTraceTransactions() + : this.fetchEtherscanTransactions() + ) + .then((transactions) => { + this.setHashes(transactions); + }) + .catch((error) => { + console.warn('getTransactions', error); + this.setLoading(false); + }); + } + + fetchEtherscanTransactions () { + return etherscan.account.transactions(this.address, 0, this.isTest); + } + + fetchTraceTransactions () { + return Promise + .all([ + this._api.trace.filter({ + fromAddress: this.address, + fromBlock: 0 + }), + this._api.trace.filter({ + fromBlock: 0, + toAddress: this.address + }) + ]) + .then(([fromTransactions, toTransactions]) => { + return fromTransactions + .concat(toTransactions) + .map((transaction) => { + return { + blockNumber: transaction.blockNumber, + from: transaction.action.from, + hash: transaction.transactionHash, + to: transaction.action.to + }; + }); + }); + } +} diff --git a/js/src/views/Account/Transactions/store.spec.js b/js/src/views/Account/Transactions/store.spec.js new file mode 100644 index 000000000..a25b58d29 --- /dev/null +++ b/js/src/views/Account/Transactions/store.spec.js @@ -0,0 +1,193 @@ +// Copyright 2015, 2016 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 BigNumber from 'bignumber.js'; +import sinon from 'sinon'; + +import { mockget as mockEtherscan } from '~/3rdparty/etherscan/helpers.spec.js'; +import { ADDRESS, createApi } from './transactions.test.js'; + +import Store from './store'; + +let api; +let store; + +function createStore () { + api = createApi(); + store = new Store(api); + + return store; +} + +function mockQuery () { + mockEtherscan([{ + query: { + module: 'account', + action: 'txlist', + address: ADDRESS, + offset: 25, + page: 1, + sort: 'desc' + }, + reply: [{ hash: '123' }] + }], true); +} + +describe('views/Account/Transactions/store', () => { + beforeEach(() => { + mockQuery(); + createStore(); + }); + + describe('constructor', () => { + it('sets the api', () => { + expect(store._api).to.deep.equals(api); + }); + + it('starts with isLoading === false', () => { + expect(store.isLoading).to.be.false; + }); + + it('starts with isTracing === false', () => { + expect(store.isTracing).to.be.false; + }); + }); + + describe('@action', () => { + describe('setHashes', () => { + it('clears the loading state', () => { + store.setLoading(true); + store.setHashes([]); + expect(store.isLoading).to.be.false; + }); + + it('sets the hashes from the transactions', () => { + store.setHashes([{ hash: '123' }, { hash: '456' }]); + expect(store.txHashes.peek()).to.deep.equal(['123', '456']); + }); + }); + + describe('setAddress', () => { + it('sets the address', () => { + store.setAddress(ADDRESS); + expect(store.address).to.equal(ADDRESS); + }); + }); + + describe('setLoading', () => { + it('sets the isLoading flag', () => { + store.setLoading(true); + expect(store.isLoading).to.be.true; + }); + }); + + describe('setTest', () => { + it('sets the isTest flag', () => { + store.setTest(true); + expect(store.isTest).to.be.true; + }); + }); + + describe('setTracing', () => { + it('sets the isTracing flag', () => { + store.setTracing(true); + expect(store.isTracing).to.be.true; + }); + }); + + describe('updateProps', () => { + it('retrieves transactions once updated', () => { + sinon.spy(store, 'getTransactions'); + store.updateProps({}); + + expect(store.getTransactions).to.have.been.called; + store.getTransactions.restore(); + }); + }); + }); + + describe('operations', () => { + describe('getTransactions', () => { + it('retrieves the hashes via etherscan', () => { + sinon.spy(store, 'fetchEtherscanTransactions'); + store.setAddress(ADDRESS); + store.setTest(true); + store.setTracing(false); + + return store.getTransactions().then(() => { + expect(store.fetchEtherscanTransactions).to.have.been.called; + expect(store.txHashes.peek()).to.deep.equal(['123']); + store.fetchEtherscanTransactions.restore(); + }); + }); + + it('retrieves the hashes via tracing', () => { + sinon.spy(store, 'fetchTraceTransactions'); + store.setAddress(ADDRESS); + store.setTest(true); + store.setTracing(true); + + return store.getTransactions().then(() => { + expect(store.fetchTraceTransactions).to.have.been.called; + expect(store.txHashes.peek()).to.deep.equal(['123', '098']); + store.fetchTraceTransactions.restore(); + }); + }); + }); + + describe('fetchEtherscanTransactions', () => { + it('retrieves the transactions', () => { + store.setAddress(ADDRESS); + store.setTest(true); + + return store.fetchEtherscanTransactions().then((transactions) => { + expect(transactions).to.deep.equal([{ + blockNumber: new BigNumber(0), + from: '', + hash: '123', + timeStamp: undefined, + to: '', + value: undefined + }]); + }); + }); + }); + + describe('fetchTraceTransactions', () => { + it('retrieves the transactions', () => { + store.setAddress(ADDRESS); + store.setTest(true); + + return store.fetchTraceTransactions().then((transactions) => { + expect(transactions).to.deep.equal([ + { + blockNumber: undefined, + from: undefined, + hash: '123', + to: undefined + }, + { + blockNumber: undefined, + from: undefined, + hash: '098', + to: undefined + } + ]); + }); + }); + }); + }); +}); diff --git a/js/src/views/Account/Transactions/transactions.js b/js/src/views/Account/Transactions/transactions.js index eb11e8def..5e48d5c5c 100644 --- a/js/src/views/Account/Transactions/transactions.js +++ b/js/src/views/Account/Transactions/transactions.js @@ -14,15 +14,18 @@ // 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, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import etherscan from '~/3rdparty/etherscan'; import { Container, TxList, Loading } from '~/ui'; +import Store from './store'; import styles from './transactions.css'; +@observer class Transactions extends Component { static contextTypes = { api: PropTypes.object.isRequired @@ -34,34 +37,35 @@ class Transactions extends Component { traceMode: PropTypes.bool } - state = { - hashes: [], - loading: true, - callInfo: {} - } + store = new Store(this.context.api); - componentDidMount () { - this.getTransactions(this.props); + componentWillMount () { + this.store.updateProps(this.props); } componentWillReceiveProps (newProps) { if (this.props.traceMode === undefined && newProps.traceMode !== undefined) { - this.getTransactions(newProps); + this.store.updateProps(newProps); return; } - const hasChanged = [ 'isTest', 'address' ] + const hasChanged = ['isTest', 'address'] .map(key => newProps[key] !== this.props[key]) .reduce((truth, keyTruth) => truth || keyTruth, false); if (hasChanged) { - this.getTransactions(newProps); + this.store.updateProps(newProps); } } render () { return ( - + + }> { this.renderTransactionList() } { this.renderEtherscanFooter() } @@ -69,10 +73,9 @@ class Transactions extends Component { } renderTransactionList () { - const { address } = this.props; - const { hashes, loading } = this.state; + const { address, isLoading, txHashes } = this.store; - if (loading) { + if (isLoading) { return ( ); @@ -81,85 +84,29 @@ class Transactions extends Component { return ( ); } renderEtherscanFooter () { - const { traceMode } = this.props; + const { isTracing } = this.store; - if (traceMode) { + if (isTracing) { return null; } return (
- Transaction list powered by etherscan.io + etherscan.io + } } />
); } - - getTransactions = (props) => { - const { isTest, address, traceMode } = props; - - // Don't fetch the transactions if we don't know in which - // network we are yet... - if (isTest === undefined) { - return; - } - - return this - .fetchTransactions(isTest, address, traceMode) - .then((transactions) => { - this.setState({ - hashes: transactions.map((transaction) => transaction.hash), - loading: false - }); - }); - } - - fetchTransactions = (isTest, address, traceMode) => { - // if (traceMode) { - // return this.fetchTraceTransactions(address); - // } - - return this.fetchEtherscanTransactions(isTest, address); - } - - fetchEtherscanTransactions = (isTest, address) => { - return etherscan.account - .transactions(address, 0, isTest) - .catch((error) => { - console.error('getTransactions', error); - }); - } - - fetchTraceTransactions = (address) => { - return Promise - .all([ - this.context.api.trace - .filter({ - fromBlock: 0, - fromAddress: address - }), - this.context.api.trace - .filter({ - fromBlock: 0, - toAddress: address - }) - ]) - .then(([fromTransactions, toTransactions]) => { - const transactions = [].concat(fromTransactions, toTransactions); - - return transactions.map(transaction => ({ - from: transaction.action.from, - to: transaction.action.to, - blockNumber: transaction.blockNumber, - hash: transaction.transactionHash - })); - }); - } } function mapStateToProps (state) { diff --git a/js/src/views/Account/Transactions/transactions.spec.js b/js/src/views/Account/Transactions/transactions.spec.js new file mode 100644 index 000000000..53f55b524 --- /dev/null +++ b/js/src/views/Account/Transactions/transactions.spec.js @@ -0,0 +1,55 @@ +// Copyright 2015, 2016 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 { shallow } from 'enzyme'; +import React from 'react'; + +import { ADDRESS, createApi, createRedux } from './transactions.test.js'; + +import Transactions from './'; + +let component; +let instance; + +function render (props) { + component = shallow( + , + { context: { store: createRedux() } } + ).find('Transactions').shallow({ context: { api: createApi() } }); + instance = component.instance(); + + return component; +} + +describe('views/Account/Transactions', () => { + it('renders defaults', () => { + expect(render()).to.be.ok; + }); + + describe('renderTransactionList', () => { + it('renders Loading when isLoading === true', () => { + instance.store.setLoading(true); + expect(instance.renderTransactionList().type).to.match(/Loading/); + }); + + it('renders TxList when isLoading === true', () => { + instance.store.setLoading(false); + expect(instance.renderTransactionList().type).to.match(/Connect/); + }); + }); +}); diff --git a/js/src/views/Account/Transactions/transactions.test.js b/js/src/views/Account/Transactions/transactions.test.js new file mode 100644 index 000000000..4b7b679b6 --- /dev/null +++ b/js/src/views/Account/Transactions/transactions.test.js @@ -0,0 +1,31 @@ +// Copyright 2015, 2016 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 { ADDRESS, createRedux } from '../account.test.js'; + +function createApi () { + return { + trace: { + filter: (options) => Promise.resolve([{ transactionHash: options.fromAddress ? '123' : '098', action: {} }]) + } + }; +} + +export { + ADDRESS, + createApi, + createRedux +}; diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js index f274d8fbe..e3c4d9776 100644 --- a/js/src/views/Account/account.js +++ b/js/src/views/Account/account.js @@ -14,47 +14,38 @@ // 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, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import ActionDelete from 'material-ui/svg-icons/action/delete'; -import ContentCreate from 'material-ui/svg-icons/content/create'; -import ContentSend from 'material-ui/svg-icons/content/send'; -import LockIcon from 'material-ui/svg-icons/action/lock'; -import VerifyIcon from 'material-ui/svg-icons/action/verified-user'; - -import { EditMeta, DeleteAccount, Shapeshift, Verification, Transfer, PasswordManager } from '~/modals'; -import { Actionbar, Button, Page } from '~/ui'; import shapeshiftBtn from '~/../assets/images/shapeshift-btn.png'; - -import Header from './Header'; -import Transactions from './Transactions'; +import { EditMeta, DeleteAccount, Shapeshift, Verification, Transfer, PasswordManager } from '~/modals'; import { setVisibleAccounts } from '~/redux/providers/personalActions'; import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions'; +import { Actionbar, Button, Page } from '~/ui'; +import { DeleteIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon } from '~/ui/Icons'; +import Header from './Header'; +import Store from './store'; +import Transactions from './Transactions'; import styles from './account.css'; +@observer class Account extends Component { static propTypes = { - setVisibleAccounts: PropTypes.func.isRequired, fetchCertifiers: PropTypes.func.isRequired, fetchCertifications: PropTypes.func.isRequired, images: PropTypes.object.isRequired, + setVisibleAccounts: PropTypes.func.isRequired, - params: PropTypes.object, accounts: PropTypes.object, - balances: PropTypes.object + balances: PropTypes.object, + params: PropTypes.object } - state = { - showDeleteDialog: false, - showEditDialog: false, - showFundDialog: false, - showVerificationDialog: false, - showTransferDialog: false, - showPasswordDialog: false - } + store = new Store(); componentDidMount () { this.props.fetchCertifiers(); @@ -76,7 +67,8 @@ class Account extends Component { setVisibleAccounts (props = this.props) { const { params, setVisibleAccounts, fetchCertifications } = props; - const addresses = [ params.address ]; + const addresses = [params.address]; + setVisibleAccounts(addresses); fetchCertifications(params.address); } @@ -97,15 +89,14 @@ class Account extends Component { { this.renderDeleteDialog(account) } { this.renderEditDialog(account) } { this.renderFundDialog() } + { this.renderPasswordDialog(account) } + { this.renderTransferDialog(account, balance) } { this.renderVerificationDialog() } - { this.renderTransferDialog() } - { this.renderPasswordDialog() } - { this.renderActionbar() } + { this.renderActionbar(balance) }
+ balance={ balance } /> @@ -114,86 +105,108 @@ class Account extends Component { ); } - renderActionbar () { - const { address } = this.props.params; - const { balances } = this.props; - const balance = balances[address]; - + renderActionbar (balance) { const showTransferButton = !!(balance && balance.tokens); const buttons = [
); } const confirmations = blockNumber.minus(transaction.blockNumber).plus(1); const value = Math.min(confirmations.toNumber(), maxConfirmations); - let count; - if (confirmations.gt(maxConfirmations)) { - count = confirmations.toFormat(0); - } else { - count = confirmations.toFormat(0) + `/${maxConfirmations}`; + + let count = confirmations.toFormat(0); + if (confirmations.lte(maxConfirmations)) { + count = `${count}/${maxConfirmations}`; } - const unit = value === 1 ? 'confirmation' : 'confirmations'; return (
@@ -121,10 +124,17 @@ class TxHash extends Component { max={ maxConfirmations } value={ value } color='white' - mode='determinate' - /> + mode='determinate' />
- { count } { unit } + + +
); @@ -138,15 +148,17 @@ class TxHash extends Component { return; } - this.setState({ blockNumber }); - - api.eth + return api.eth .getTransactionReceipt(hash) .then((transaction) => { - this.setState({ transaction }); + this.setState({ + blockNumber, + transaction + }); }) .catch((error) => { console.warn('onBlockNumber', error); + this.setState({ blockNumber }); }); } } diff --git a/js/src/ui/TxHash/txHash.spec.js b/js/src/ui/TxHash/txHash.spec.js new file mode 100644 index 000000000..7f47363ee --- /dev/null +++ b/js/src/ui/TxHash/txHash.spec.js @@ -0,0 +1,132 @@ +// Copyright 2015, 2016 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 BigNumber from 'bignumber.js'; +import { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; + +import TxHash from './'; + +const TXHASH = '0xabcdef123454321abcdef'; + +let api; +let blockNumber; +let callback; +let component; +let instance; + +function createApi () { + blockNumber = new BigNumber(100); + api = { + eth: { + getTransactionReceipt: (hash) => { + return Promise.resolve({ + blockNumber: new BigNumber(100), + hash + }); + } + }, + nextBlock: (increment = 1) => { + blockNumber = blockNumber.plus(increment); + return callback(null, blockNumber); + }, + subscribe: (type, _callback) => { + callback = _callback; + return callback(null, blockNumber).then(() => { + return Promise.resolve(1); + }); + }, + unsubscribe: sinon.stub().resolves(true) + }; + + return api; +} + +function createRedux () { + return { + dispatch: sinon.stub(), + subscribe: sinon.stub(), + getState: () => { + return { + nodeStatus: { isTest: true } + }; + } + }; +} + +function render (props) { + const baseComponent = shallow( + , + { context: { store: createRedux() } } + ); + component = baseComponent.find('TxHash').shallow({ context: { api: createApi() } }); + instance = component.instance(); + + return component; +} + +describe('ui/TxHash', () => { + beforeEach(() => { + render(); + }); + + it('renders defaults', () => { + expect(component).to.be.ok; + }); + + it('renders the summary', () => { + expect(component.find('p').find('FormattedMessage').props().id).to.equal('ui.txHash.posted'); + }); + + describe('renderConfirmations', () => { + describe('with no transaction retrieved', () => { + let child; + + beforeEach(() => { + child = shallow(instance.renderConfirmations()); + }); + + it('renders indeterminate progressbar', () => { + expect(child.find('LinearProgress[mode="indeterminate"]')).to.have.length(1); + }); + + it('renders waiting text', () => { + expect(child.find('FormattedMessage').props().id).to.equal('ui.txHash.waiting'); + }); + }); + + describe('with transaction retrieved', () => { + let child; + + beforeEach(() => { + return instance.componentDidMount().then(() => { + child = shallow(instance.renderConfirmations()); + }); + }); + + it('renders determinate progressbar', () => { + expect(child.find('LinearProgress[mode="determinate"]')).to.have.length(1); + }); + + it('renders confirmation text', () => { + expect(child.find('FormattedMessage').props().id).to.equal('ui.txHash.confirmations'); + }); + }); + }); +}); diff --git a/js/src/ui/TxList/TxRow/txRow.spec.js b/js/src/ui/TxList/TxRow/txRow.spec.js index ddee9024c..030ff4432 100644 --- a/js/src/ui/TxList/TxRow/txRow.spec.js +++ b/js/src/ui/TxList/TxRow/txRow.spec.js @@ -25,7 +25,7 @@ import TxRow from './txRow'; const api = new Api({ execute: sinon.stub() }); -function renderShallow (props) { +function render (props) { return shallow( , @@ -33,7 +33,7 @@ function renderShallow (props) { ); } -describe('ui/TxRow', () => { +describe('ui/TxList/TxRow', () => { describe('rendering', () => { it('renders defaults', () => { const block = { @@ -45,7 +45,7 @@ describe('ui/TxRow', () => { value: new BigNumber(1) }; - expect(renderShallow({ block, tx })).to.be.ok; + expect(render({ address: '0x123', block, isTest: true, tx })).to.be.ok; }); }); }); diff --git a/js/src/ui/TxList/txList.spec.js b/js/src/ui/TxList/txList.spec.js index 88012888c..11367fbce 100644 --- a/js/src/ui/TxList/txList.spec.js +++ b/js/src/ui/TxList/txList.spec.js @@ -36,7 +36,7 @@ const STORE = { } }; -function renderShallow (props) { +function render (props) { return shallow( { describe('rendering', () => { it('renders defaults', () => { - expect(renderShallow()).to.be.ok; + expect(render({ address: '0x123', hashes: [] })).to.be.ok; }); }); }); From d16ab5eac50963c0ef9fb5bfa9e33a75f68cf914 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Thu, 5 Jan 2017 12:06:58 +0100 Subject: [PATCH 20/22] Show contract parameters in MethodDecoding (#4024) * Add decoding of Inner Contract Deployment params #3715 * Fixed TypedInput when formatted value * Fix TypedInput * PR Grumble * Add test to `Param.toParams` --- js/src/abi/spec/param.js | 8 +- js/src/abi/spec/param.spec.js | 9 ++ js/src/api/util/decode.js | 12 +- js/src/ui/Form/AddressSelect/addressSelect.js | 6 +- js/src/ui/Form/InputAddress/inputAddress.js | 11 +- .../InputAddressSelect/inputAddressSelect.js | 4 +- js/src/ui/Form/TypedInput/typedInput.js | 45 +++++-- js/src/ui/MethodDecoding/methodDecoding.js | 56 ++++---- .../ui/MethodDecoding/methodDecodingStore.js | 120 +++++++++++++++--- 9 files changed, 207 insertions(+), 64 deletions(-) diff --git a/js/src/abi/spec/param.js b/js/src/abi/spec/param.js index 88696ceed..d7a85c009 100644 --- a/js/src/abi/spec/param.js +++ b/js/src/abi/spec/param.js @@ -31,6 +31,12 @@ export default class Param { } static toParams (params) { - return params.map((param) => new Param(param.name, param.type)); + return params.map((param) => { + if (param instanceof Param) { + return param; + } + + return new Param(param.name, param.type); + }); } } diff --git a/js/src/abi/spec/param.spec.js b/js/src/abi/spec/param.spec.js index 9957df909..c1dcddeb5 100644 --- a/js/src/abi/spec/param.spec.js +++ b/js/src/abi/spec/param.spec.js @@ -34,5 +34,14 @@ describe('abi/spec/Param', () => { expect(params[0].name).to.equal('foo'); expect(params[0].kind.type).to.equal('uint'); }); + + it('converts only if needed', () => { + const _params = Param.toParams([{ name: 'foo', type: 'uint' }]); + const params = Param.toParams(_params); + + expect(params.length).to.equal(1); + expect(params[0].name).to.equal('foo'); + expect(params[0].kind.type).to.equal('uint'); + }); }); }); diff --git a/js/src/api/util/decode.js b/js/src/api/util/decode.js index 0e0164bec..d0cea05c1 100644 --- a/js/src/api/util/decode.js +++ b/js/src/api/util/decode.js @@ -26,7 +26,9 @@ export function decodeCallData (data) { if (data.substr(0, 2) === '0x') { return decodeCallData(data.slice(2)); - } else if (data.length < 8) { + } + + if (data.length < 8) { throw new Error('Input to decodeCallData should be method signature + data'); } @@ -42,10 +44,14 @@ export function decodeCallData (data) { export function decodeMethodInput (methodAbi, paramdata) { if (!methodAbi) { throw new Error('decodeMethodInput should receive valid method-specific ABI'); - } else if (paramdata && paramdata.length) { + } + + if (paramdata && paramdata.length) { if (!isHex(paramdata)) { throw new Error('Input to decodeMethodInput should be a hex value'); - } else if (paramdata.substr(0, 2) === '0x') { + } + + if (paramdata.substr(0, 2) === '0x') { return decodeMethodInput(methodAbi, paramdata.slice(2)); } } diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index ba48b6489..692ff4285 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -60,6 +60,7 @@ class AddressSelect extends Component { // Optional props allowCopy: PropTypes.bool, allowInput: PropTypes.bool, + className: PropTypes.string, disabled: PropTypes.bool, error: nodeOrStringProptype(), hint: nodeOrStringProptype(), @@ -123,13 +124,14 @@ class AddressSelect extends Component { renderInput () { const { focused } = this.state; - const { accountsInfo, allowCopy, disabled, error, hint, label, readOnly, value } = this.props; + const { accountsInfo, allowCopy, className, disabled, error, hint, label, readOnly, value } = this.props; const input = ( + } + { ...props } + /> { icon } ); diff --git a/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js b/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js index 60a0f8d1b..f5b218694 100644 --- a/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js +++ b/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js @@ -27,6 +27,7 @@ class InputAddressSelect extends Component { contracts: PropTypes.object.isRequired, allowCopy: PropTypes.bool, + className: PropTypes.string, error: PropTypes.string, hint: PropTypes.string, label: PropTypes.string, @@ -36,13 +37,14 @@ class InputAddressSelect extends Component { }; render () { - const { accounts, allowCopy, contacts, contracts, label, hint, error, value, onChange, readOnly } = this.props; + const { accounts, allowCopy, className, contacts, contracts, label, hint, error, value, onChange, readOnly } = this.props; return ( . -import BigNumber from 'bignumber.js'; import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import CircularProgress from 'material-ui/CircularProgress'; -import { Input, InputAddress } from '../Form'; +import { TypedInput, InputAddress } from '../Form'; import MethodDecodingStore from './methodDecodingStore'; import styles from './methodDecoding.css'; @@ -245,6 +244,7 @@ class MethodDecoding extends Component { renderDeploy () { const { historic, transaction } = this.props; + const { methodInputs } = this.state; if (!historic) { return ( @@ -261,6 +261,14 @@ class MethodDecoding extends Component { { this.renderAddressName(transaction.creates, false) } + +
+ { methodInputs && methodInputs.length ? 'with the following parameters:' : ''} +
+ +
+ { this.renderInputs() } +
); } @@ -364,39 +372,31 @@ class MethodDecoding extends Component { renderInputs () { const { methodInputs } = this.state; - return methodInputs.map((input, index) => { - switch (input.type) { - case 'address': - return ( - - ); + if (!methodInputs || methodInputs.length === 0) { + return null; + } - default: - return ( - - ); - } + const inputs = methodInputs.map((input, index) => { + return ( + + ); }); + + return inputs; } renderValue (value) { const { api } = this.context; - if (api.util.isInstanceOf(value, BigNumber)) { - return value.toFormat(0); - } else if (api.util.isArray(value)) { + if (api.util.isArray(value)) { return api.util.bytesToHex(value); } diff --git a/js/src/ui/MethodDecoding/methodDecodingStore.js b/js/src/ui/MethodDecoding/methodDecodingStore.js index 5d518d3a9..b31412c21 100644 --- a/js/src/ui/MethodDecoding/methodDecodingStore.js +++ b/js/src/ui/MethodDecoding/methodDecodingStore.js @@ -18,6 +18,8 @@ import Contracts from '~/contracts'; import Abi from '~/abi'; import * as abis from '~/contracts/abi'; +import { decodeMethodInput } from '~/api/util/decode'; + const CONTRACT_CREATE = '0x60606040'; let instance = null; @@ -26,6 +28,8 @@ export default class MethodDecodingStore { api = null; + _bytecodes = {}; + _contractsAbi = {}; _isContract = {}; _methods = {}; @@ -46,12 +50,17 @@ export default class MethodDecodingStore { if (!contract || !contract.meta || !contract.meta.abi) { return; } - this.loadFromAbi(contract.meta.abi); + this.loadFromAbi(contract.meta.abi, contract.address); }); } - loadFromAbi (_abi) { + loadFromAbi (_abi, contractAddress) { const abi = new Abi(_abi); + + if (contractAddress && abi) { + this._contractsAbi[contractAddress] = abi; + } + abi .functions .map((f) => ({ sign: f.signature, abi: f.abi })) @@ -111,6 +120,7 @@ export default class MethodDecodingStore { const contractAddress = isReceived ? transaction.from : transaction.to; const input = transaction.input || transaction.data; + result.input = input; result.received = isReceived; // No input, should be a ETH transfer @@ -118,17 +128,20 @@ export default class MethodDecodingStore { return Promise.resolve(result); } - try { - const { signature } = this.api.util.decodeCallData(input); + let signature; - if (signature === CONTRACT_CREATE || transaction.creates) { - result.contract = true; - return Promise.resolve({ ...result, deploy: true }); - } + try { + const decodeCallDataResult = this.api.util.decodeCallData(input); + signature = decodeCallDataResult.signature; } catch (e) {} + // Contract deployment + if (!signature || signature === CONTRACT_CREATE || transaction.creates) { + return this.decodeContractCreation(result, contractAddress || transaction.creates); + } + return this - .isContract(contractAddress || transaction.creates) + .isContract(contractAddress) .then((isContract) => { result.contract = isContract; @@ -140,11 +153,6 @@ export default class MethodDecodingStore { result.signature = signature; result.params = paramdata; - // Contract deployment - if (!signature) { - return Promise.resolve({ ...result, deploy: true }); - } - return this .fetchMethodAbi(signature) .then((abi) => { @@ -173,6 +181,68 @@ export default class MethodDecodingStore { }); } + decodeContractCreation (data, contractAddress) { + const result = { + ...data, + contract: true, + deploy: true + }; + + const { input } = data; + const abi = this._contractsAbi[contractAddress]; + + if (!input || !abi || !abi.constructors || abi.constructors.length === 0) { + return Promise.resolve(result); + } + + const constructorAbi = abi.constructors[0]; + + const rawInput = /^(?:0x)?(.*)$/.exec(input)[1]; + + return this + .getCode(contractAddress) + .then((code) => { + if (!code || /^(0x)0*?$/.test(code)) { + return result; + } + + const rawCode = /^(?:0x)?(.*)$/.exec(code)[1]; + const codeOffset = rawInput.indexOf(rawCode); + + if (codeOffset === -1) { + return result; + } + + // Params are the last bytes of the transaction Input + // (minus the bytecode). It seems that they are repeated + // twice + const params = rawInput.slice(codeOffset + rawCode.length); + const paramsBis = params.slice(params.length / 2); + + let decodedInputs; + + try { + decodedInputs = decodeMethodInput(constructorAbi, params); + } catch (e) {} + + try { + if (!decodedInputs) { + decodedInputs = decodeMethodInput(constructorAbi, paramsBis); + } + } catch (e) {} + + if (decodedInputs && decodedInputs.length > 0) { + result.inputs = decodedInputs + .map((value, index) => { + const type = constructorAbi.inputs[index].kind.type; + return { type, value }; + }); + } + + return result; + }); + } + fetchMethodAbi (signature) { if (this._methods[signature] !== undefined) { return Promise.resolve(this._methods[signature]); @@ -209,7 +279,7 @@ export default class MethodDecodingStore { return Promise.resolve(this._isContract[contractAddress]); } - this._isContract[contractAddress] = this.api.eth + this._isContract[contractAddress] = this .getCode(contractAddress) .then((bytecode) => { // Is a contract if the address contains *valid* bytecode @@ -222,4 +292,24 @@ export default class MethodDecodingStore { return Promise.resolve(this._isContract[contractAddress]); } + getCode (contractAddress) { + // If zero address, resolve to '0x' + if (!contractAddress || /^(0x)?0*$/.test(contractAddress)) { + return Promise.resolve('0x'); + } + + if (this._bytecodes[contractAddress]) { + return Promise.resolve(this._bytecodes[contractAddress]); + } + + this._bytecodes[contractAddress] = this.api.eth + .getCode(contractAddress) + .then((bytecode) => { + this._bytecodes[contractAddress] = bytecode; + return this._bytecodes[contractAddress]; + }); + + return Promise.resolve(this._bytecodes[contractAddress]); + } + } From 1ef67f68ed28dde17eb651676fb6d5080467e11b Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Thu, 5 Jan 2017 12:07:10 +0100 Subject: [PATCH 21/22] Starting on homestead shows reload snackbar (#4043) * Fix issue where starting on homestead showed reload * Align snackbar timing with errors (60s) --- js/src/redux/providers/chainMiddleware.js | 7 +- .../redux/providers/chainMiddleware.spec.js | 86 +++++++++++++++++++ js/src/redux/providers/statusReducer.js | 8 +- 3 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 js/src/redux/providers/chainMiddleware.spec.js diff --git a/js/src/redux/providers/chainMiddleware.js b/js/src/redux/providers/chainMiddleware.js index 82281f3b8..77c757da6 100644 --- a/js/src/redux/providers/chainMiddleware.js +++ b/js/src/redux/providers/chainMiddleware.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import { showSnackbar } from './snackbarActions'; +import { DEFAULT_NETCHAIN } from './statusReducer'; export default class ChainMiddleware { toMiddleware () { @@ -23,11 +24,11 @@ export default class ChainMiddleware { const { collection } = action; if (collection && collection.netChain) { - const chain = collection.netChain; + const newChain = collection.netChain; const { nodeStatus } = store.getState(); - if (chain !== nodeStatus.netChain) { - store.dispatch(showSnackbar(`Switched to ${chain}. Please reload the page.`, 5000)); + if (newChain !== nodeStatus.netChain && nodeStatus.netChain !== DEFAULT_NETCHAIN) { + store.dispatch(showSnackbar(`Switched to ${newChain}. Please reload the page.`, 60000)); } } } diff --git a/js/src/redux/providers/chainMiddleware.spec.js b/js/src/redux/providers/chainMiddleware.spec.js new file mode 100644 index 000000000..ed2d5eca6 --- /dev/null +++ b/js/src/redux/providers/chainMiddleware.spec.js @@ -0,0 +1,86 @@ +// Copyright 2015, 2016 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 sinon from 'sinon'; + +import { initialState as defaultNodeStatusState } from './statusReducer'; +import ChainMiddleware from './chainMiddleware'; + +let middleware; +let next; +let store; + +function createMiddleware (collection = {}) { + middleware = new ChainMiddleware().toMiddleware(); + next = sinon.stub(); + store = { + dispatch: sinon.stub(), + getState: () => { + return { + nodeStatus: Object.assign({}, defaultNodeStatusState, collection) + }; + } + }; + + return middleware; +} + +function callMiddleware (action) { + return middleware(store)(next)(action); +} + +describe('reduxs/providers/ChainMiddleware', () => { + describe('next action', () => { + beforeEach(() => { + createMiddleware(); + }); + + it('calls next with matching actiontypes', () => { + callMiddleware({ type: 'statusCollection' }); + + expect(next).to.have.been.calledWithMatch({ type: 'statusCollection' }); + }); + + it('calls next with non-matching actiontypes', () => { + callMiddleware({ type: 'nonMatchingType' }); + + expect(next).to.have.been.calledWithMatch({ type: 'nonMatchingType' }); + }); + }); + + describe('chain switching', () => { + it('does not dispatch when moving from the initial/unknown chain', () => { + createMiddleware(); + callMiddleware({ type: 'statusCollection', collection: { netChain: 'homestead' } }); + + expect(store.dispatch).not.to.have.been.called; + }); + + it('does not dispatch when moving to the same chain', () => { + createMiddleware({ netChain: 'homestead' }); + callMiddleware({ type: 'statusCollection', collection: { netChain: 'homestead' } }); + + expect(store.dispatch).not.to.have.been.called; + }); + + it('does dispatch when moving between chains', () => { + createMiddleware({ netChain: 'homestead' }); + callMiddleware({ type: 'statusCollection', collection: { netChain: 'ropsten' } }); + + expect(store.dispatch).to.have.been.called; + }); + }); +}); diff --git a/js/src/redux/providers/statusReducer.js b/js/src/redux/providers/statusReducer.js index 17186b012..4bef27b1b 100644 --- a/js/src/redux/providers/statusReducer.js +++ b/js/src/redux/providers/statusReducer.js @@ -17,6 +17,7 @@ import BigNumber from 'bignumber.js'; import { handleActions } from 'redux-actions'; +const DEFAULT_NETCHAIN = '(unknown)'; const initialState = { blockNumber: new BigNumber(0), blockTimestamp: new Date(), @@ -32,7 +33,7 @@ const initialState = { gasLimit: new BigNumber(0), hashrate: new BigNumber(0), minGasPrice: new BigNumber(0), - netChain: 'ropsten', + netChain: DEFAULT_NETCHAIN, netPeers: { active: new BigNumber(0), connected: new BigNumber(0), @@ -82,3 +83,8 @@ export default handleActions({ return Object.assign({}, state, { refreshStatus }); } }, initialState); + +export { + DEFAULT_NETCHAIN, + initialState +}; From e7e561024a4170d4c9badf040127cf1009cfc1e5 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Thu, 5 Jan 2017 11:15:57 +0000 Subject: [PATCH 22/22] [ci skip] js-precompiled 20170105-111302 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2fee4b290..17928f75b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1500,7 +1500,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#9fc9d894356b135c66cc99bf66ec2a713b5f0a8c" +source = "git+https://github.com/ethcore/js-precompiled.git#257b3ce8aaa6797507592200dc78b29b8a305c3f" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index 29bb70791..79e8d473e 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.2.167", + "version": "0.2.168", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ",