diff --git a/Cargo.lock b/Cargo.lock index 08edcc166..776c26c42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1503,7 +1503,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#257b3ce8aaa6797507592200dc78b29b8a305c3f" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] 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/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 +} 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 3fb04fc36..f9a4b6f6a 100644 --- a/ethcore/src/spec/spec.rs +++ b/ethcore/src/spec/spec.rs @@ -58,7 +58,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(), chain_id: if let Some(n) = p.chain_id { n.into() } else { p.network_id.into() }, diff --git a/ethcore/src/state/account.rs b/ethcore/src/state/account.rs index 49cebd550..63e8ff9de 100644 --- a/ethcore/src/state/account.rs +++ b/ethcore/src/state/account.rs @@ -20,6 +20,7 @@ use util::*; use pod_account::*; use rlp::*; use lru_cache::LruCache; +use basic_account::BasicAccount; use std::cell::{RefCell, Cell}; @@ -53,6 +54,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 +127,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/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; diff --git a/js/package.json b/js/package.json index 2ecad7269..79e8d473e 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.2.165", + "version": "0.2.168", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", @@ -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", @@ -138,6 +139,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/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/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); + }); + }); + }); }); 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/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/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/api/util/identity.js b/js/src/api/util/identity.js index e4a95891f..48a683989 100644 --- a/js/src/api/util/identity.js +++ b/js/src/api/util/identity.js @@ -21,7 +21,7 @@ const TEST_ENV = process.env.NODE_ENV === 'test'; export function createIdentityImg (address, scale = 8) { return TEST_ENV - ? '' + ? 'test-createIdentityImg' : blockies({ seed: (address || '').toLowerCase(), size: 8, 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/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 +}; 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/BlockStatus/blockStatus.js b/js/src/ui/BlockStatus/blockStatus.js index f50c7a685..47ee1a1c8 100644 --- a/js/src/ui/BlockStatus/blockStatus.js +++ b/js/src/ui/BlockStatus/blockStatus.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; @@ -39,7 +40,12 @@ class BlockStatus extends Component { if (!syncing) { return (
- { blockNumber.toFormat() } best block +
); } @@ -47,26 +53,45 @@ class BlockStatus extends Component { if (syncing.warpChunksAmount && syncing.warpChunksProcessed && !syncing.warpChunksAmount.eq(syncing.warpChunksProcessed)) { return (
- { syncing.warpChunksProcessed.mul(100).div(syncing.warpChunksAmount).toFormat(2) }% warp restore +
); } + let syncStatus = null; let warpStatus = null; + if (syncing.currentBlock && syncing.highestBlock) { + syncStatus = ( + + + + ); + } + if (syncing.blockGap) { const [first, last] = syncing.blockGap; warpStatus = ( - , { first.mul(100).div(last).toFormat(2) }% historic - ); - } - - let syncStatus = null; - - if (syncing && syncing.currentBlock && syncing.highestBlock) { - syncStatus = ( - { syncing.currentBlock.toFormat() }/{ syncing.highestBlock.toFormat() } syncing + + + ); } diff --git a/js/src/ui/BlockStatus/blockStatus.spec.js b/js/src/ui/BlockStatus/blockStatus.spec.js new file mode 100644 index 000000000..73358dc90 --- /dev/null +++ b/js/src/ui/BlockStatus/blockStatus.spec.js @@ -0,0 +1,94 @@ +// 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 BlockStatus from './'; + +let component; + +function createRedux (syncing = false, blockNumber = new BigNumber(123)) { + return { + dispatch: sinon.stub(), + subscribe: sinon.stub(), + getState: () => { + return { + nodeStatus: { + blockNumber, + syncing + } + }; + } + }; +} + +function render (reduxStore = createRedux(), props) { + component = shallow( + , + { context: { store: reduxStore } } + ).find('BlockStatus').shallow(); + + return component; +} + +describe('ui/BlockStatus', () => { + it('renders defaults', () => { + expect(render()).to.be.ok; + }); + + it('renders null with no blockNumber', () => { + expect(render(createRedux(false, null)).find('div')).to.have.length(0); + }); + + it('renders only the best block when syncing === false', () => { + const messages = render().find('FormattedMessage'); + + expect(messages).to.have.length(1); + expect(messages).to.have.id('ui.blockStatus.bestBlock'); + }); + + it('renders only the warp restore status when restoring', () => { + const messages = render(createRedux({ + warpChunksAmount: new BigNumber(100), + warpChunksProcessed: new BigNumber(5) + })).find('FormattedMessage'); + + expect(messages).to.have.length(1); + expect(messages).to.have.id('ui.blockStatus.warpRestore'); + }); + + it('renders the current/highest when syncing', () => { + const messages = render(createRedux({ + currentBlock: new BigNumber(123), + highestBlock: new BigNumber(456) + })).find('FormattedMessage'); + + expect(messages).to.have.length(1); + expect(messages).to.have.id('ui.blockStatus.syncStatus'); + }); + + it('renders warp blockGap when catching up', () => { + const messages = render(createRedux({ + blockGap: [new BigNumber(123), new BigNumber(456)] + })).find('FormattedMessage'); + + expect(messages).to.have.length(1); + expect(messages).to.have.id('ui.blockStatus.warpStatus'); + }); +}); diff --git a/js/src/ui/Button/button.spec.js b/js/src/ui/Button/button.spec.js index 101bb19ac..ce0a8bd38 100644 --- a/js/src/ui/Button/button.spec.js +++ b/js/src/ui/Button/button.spec.js @@ -19,7 +19,11 @@ import { shallow } from 'enzyme'; import Button from './button'; -function renderShallow (props) { +function render (props = {}) { + if (props && props.label === undefined) { + props.label = 'test'; + } + return shallow(