Merge branch 'master' into on-demand-les-request

This commit is contained in:
Robert Habermeier 2017-01-05 13:18:24 +01:00
commit 8446a8354b
103 changed files with 3605 additions and 1023 deletions

2
Cargo.lock generated
View File

@ -1503,7 +1503,7 @@ dependencies = [
[[package]] [[package]]
name = "parity-ui-precompiled" name = "parity-ui-precompiled"
version = "1.4.0" 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 = [ dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]

View File

@ -150,6 +150,11 @@ impl AccountProvider {
Ok(Address::from(address).into()) Ok(Address::from(address).into())
} }
/// Checks whether an account with a given address is present.
pub fn has_account(&self, address: Address) -> Result<bool, Error> {
Ok(self.accounts()?.iter().any(|&a| a == address))
}
/// Returns addresses of all accounts. /// Returns addresses of all accounts.
pub fn accounts(&self) -> Result<Vec<Address>, Error> { pub fn accounts(&self) -> Result<Vec<Address>, Error> {
let accounts = self.sstore.accounts()?; let accounts = self.sstore.accounts()?;

View File

@ -17,16 +17,17 @@
//! Account state encoding and decoding //! Account state encoding and decoding
use account_db::{AccountDB, AccountDBMut}; use account_db::{AccountDB, AccountDBMut};
use basic_account::BasicAccount;
use snapshot::Error; use snapshot::Error;
use util::{U256, FixedHash, H256, Bytes, HashDB, SHA3_EMPTY, SHA3_NULL_RLP}; use util::{U256, FixedHash, H256, Bytes, HashDB, SHA3_EMPTY, SHA3_NULL_RLP};
use util::trie::{TrieDB, Trie}; use util::trie::{TrieDB, Trie};
use rlp::{Rlp, RlpStream, Stream, UntrustedRlp, View}; use rlp::{RlpStream, Stream, UntrustedRlp, View};
use std::collections::HashSet; use std::collections::HashSet;
// An empty account -- these are replaced with RLP null data for a space optimization. // 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]), nonce: U256([0, 0, 0, 0]),
balance: U256([0, 0, 0, 0]), balance: U256([0, 0, 0, 0]),
storage_root: SHA3_NULL_RLP, storage_root: SHA3_NULL_RLP,
@ -59,48 +60,14 @@ impl CodeState {
} }
} }
// An alternate account structure from ::account::Account. // walk the account's storage trie, returning an RLP item containing the
#[derive(PartialEq, Clone, Debug)] // account properties and the storage.
pub struct Account { pub fn to_fat_rlp(acc: &BasicAccount, acct_db: &AccountDB, used_code: &mut HashSet<H256>) -> Result<Bytes, Error> {
nonce: U256, if acc == &ACC_EMPTY {
balance: U256,
storage_root: H256,
code_hash: H256,
}
impl Account {
// decode the account from rlp.
pub fn from_thin_rlp(rlp: &[u8]) -> Self {
let r: Rlp = Rlp::new(rlp);
Account {
nonce: r.val_at(0),
balance: r.val_at(1),
storage_root: r.val_at(2),
code_hash: r.val_at(3),
}
}
// 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);
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<H256>) -> Result<Bytes, Error> {
if self == &ACC_EMPTY {
return Ok(::rlp::NULL_RLP.to_vec()); return Ok(::rlp::NULL_RLP.to_vec());
} }
let db = TrieDB::new(acct_db, &self.storage_root)?; let db = TrieDB::new(acct_db, &acc.storage_root)?;
let mut pairs = Vec::new(); let mut pairs = Vec::new();
@ -118,18 +85,18 @@ impl Account {
let pairs_rlp = stream.out(); let pairs_rlp = stream.out();
let mut account_stream = RlpStream::new_list(5); let mut account_stream = RlpStream::new_list(5);
account_stream.append(&self.nonce) account_stream.append(&acc.nonce)
.append(&self.balance); .append(&acc.balance);
// [has_code, code_hash]. // [has_code, code_hash].
if self.code_hash == SHA3_EMPTY { if acc.code_hash == SHA3_EMPTY {
account_stream.append(&CodeState::Empty.raw()).append_empty_data(); account_stream.append(&CodeState::Empty.raw()).append_empty_data();
} else if used_code.contains(&self.code_hash) { } else if used_code.contains(&acc.code_hash) {
account_stream.append(&CodeState::Hash.raw()).append(&self.code_hash); account_stream.append(&CodeState::Hash.raw()).append(&acc.code_hash);
} else { } else {
match acct_db.get(&self.code_hash) { match acct_db.get(&acc.code_hash) {
Some(c) => { Some(c) => {
used_code.insert(self.code_hash.clone()); used_code.insert(acc.code_hash.clone());
account_stream.append(&CodeState::Inline.raw()).append(&&*c); account_stream.append(&CodeState::Inline.raw()).append(&&*c);
} }
None => { None => {
@ -142,15 +109,15 @@ impl Account {
account_stream.append_raw(&pairs_rlp, 1); account_stream.append_raw(&pairs_rlp, 1);
Ok(account_stream.out()) Ok(account_stream.out())
} }
// decode a fat rlp, and rebuild the storage trie as we go. // decode a fat rlp, and rebuild the storage trie as we go.
// returns the account structure along with its newly recovered code, // returns the account structure along with its newly recovered code,
// if it exists. // if it exists.
pub fn from_fat_rlp( pub fn from_fat_rlp(
acct_db: &mut AccountDBMut, acct_db: &mut AccountDBMut,
rlp: UntrustedRlp, rlp: UntrustedRlp,
) -> Result<(Self, Option<Bytes>), Error> { ) -> Result<(BasicAccount, Option<Bytes>), Error> {
use util::{TrieDBMut, TrieMut}; use util::{TrieDBMut, TrieMut};
// check for special case of empty account. // check for special case of empty account.
@ -194,7 +161,7 @@ impl Account {
} }
} }
let acc = Account { let acc = BasicAccount {
nonce: nonce, nonce: nonce,
balance: balance, balance: balance,
storage_root: storage_root, storage_root: storage_root,
@ -202,22 +169,12 @@ impl Account {
}; };
Ok((acc, new_code)) 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
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use account_db::{AccountDB, AccountDBMut}; use account_db::{AccountDB, AccountDBMut};
use basic_account::BasicAccount;
use tests::helpers::get_temp_state_db; use tests::helpers::get_temp_state_db;
use snapshot::tests::helpers::fill_storage; use snapshot::tests::helpers::fill_storage;
@ -227,26 +184,26 @@ mod tests {
use std::collections::HashSet; use std::collections::HashSet;
use super::{ACC_EMPTY, Account}; use super::{ACC_EMPTY, to_fat_rlp, from_fat_rlp};
#[test] #[test]
fn encoding_basic() { fn encoding_basic() {
let mut db = get_temp_state_db(); let mut db = get_temp_state_db();
let addr = Address::random(); let addr = Address::random();
let account = Account { let account = BasicAccount {
nonce: 50.into(), nonce: 50.into(),
balance: 123456789.into(), balance: 123456789.into(),
storage_root: SHA3_NULL_RLP, storage_root: SHA3_NULL_RLP,
code_hash: SHA3_EMPTY, code_hash: SHA3_EMPTY,
}; };
let thin_rlp = account.to_thin_rlp(); let thin_rlp = ::rlp::encode(&account);
assert_eq!(Account::from_thin_rlp(&thin_rlp), account); assert_eq!(::rlp::decode::<BasicAccount>(&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); 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] #[test]
@ -258,7 +215,7 @@ mod tests {
let acct_db = AccountDBMut::new(db.as_hashdb_mut(), &addr); let acct_db = AccountDBMut::new(db.as_hashdb_mut(), &addr);
let mut root = SHA3_NULL_RLP; let mut root = SHA3_NULL_RLP;
fill_storage(acct_db, &mut root, &mut H256::zero()); fill_storage(acct_db, &mut root, &mut H256::zero());
Account { BasicAccount {
nonce: 25.into(), nonce: 25.into(),
balance: 987654321.into(), balance: 987654321.into(),
storage_root: root, storage_root: root,
@ -266,12 +223,12 @@ mod tests {
} }
}; };
let thin_rlp = account.to_thin_rlp(); let thin_rlp = ::rlp::encode(&account);
assert_eq!(Account::from_thin_rlp(&thin_rlp), account); assert_eq!(::rlp::decode::<BasicAccount>(&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); 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] #[test]
@ -291,14 +248,14 @@ mod tests {
acct_db.emplace(code_hash.clone(), DBValue::from_slice(b"this is definitely code")); acct_db.emplace(code_hash.clone(), DBValue::from_slice(b"this is definitely code"));
} }
let account1 = Account { let account1 = BasicAccount {
nonce: 50.into(), nonce: 50.into(),
balance: 123456789.into(), balance: 123456789.into(),
storage_root: SHA3_NULL_RLP, storage_root: SHA3_NULL_RLP,
code_hash: code_hash, code_hash: code_hash,
}; };
let account2 = Account { let account2 = BasicAccount {
nonce: 400.into(), nonce: 400.into(),
balance: 98765432123456789usize.into(), balance: 98765432123456789usize.into(),
storage_root: SHA3_NULL_RLP, storage_root: SHA3_NULL_RLP,
@ -307,18 +264,18 @@ mod tests {
let mut used_code = HashSet::new(); 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_rlp1 = to_fat_rlp(&account1, &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_rlp2 = to_fat_rlp(&account2, &AccountDB::new(db.as_hashdb(), &addr2), &mut used_code).unwrap();
assert_eq!(used_code.len(), 1); assert_eq!(used_code.len(), 1);
let fat_rlp1 = UntrustedRlp::new(&fat_rlp1); let fat_rlp1 = UntrustedRlp::new(&fat_rlp1);
let fat_rlp2 = UntrustedRlp::new(&fat_rlp2); 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!(maybe_code.is_none());
assert_eq!(acc, account2); 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!(maybe_code, Some(b"this is definitely code".to_vec()));
assert_eq!(acc, account1); assert_eq!(acc, account1);
} }
@ -328,7 +285,7 @@ mod tests {
let mut db = get_temp_state_db(); let mut db = get_temp_state_db();
let mut used_code = HashSet::new(); 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!(to_fat_rlp(&ACC_EMPTY, &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!(from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &Address::default()), UntrustedRlp::new(&::rlp::NULL_RLP)).unwrap(), (ACC_EMPTY, None));
} }
} }

View File

@ -40,7 +40,6 @@ use util::sha3::SHA3_NULL_RLP;
use rlp::{RlpStream, Stream, UntrustedRlp, View}; use rlp::{RlpStream, Stream, UntrustedRlp, View};
use bloom_journal::Bloom; use bloom_journal::Bloom;
use self::account::Account;
use self::block::AbridgedBlock; use self::block::AbridgedBlock;
use self::io::SnapshotWriter; use self::io::SnapshotWriter;
@ -368,12 +367,12 @@ pub fn chunk_state<'a>(db: &HashDB, root: &H256, writer: &Mutex<SnapshotWriter +
// account_key here is the address' hash. // account_key here is the address' hash.
for item in account_trie.iter()? { for item in account_trie.iter()? {
let (account_key, account_data) = item?; let (account_key, account_data) = item?;
let account = Account::from_thin_rlp(&*account_data); let account = ::rlp::decode(&*account_data);
let account_key_hash = H256::from_slice(&account_key); let account_key_hash = H256::from_slice(&account_key);
let account_db = AccountDB::from_hash(db, account_key_hash); let account_db = AccountDB::from_hash(db, account_key_hash);
let fat_rlp = account.to_fat_rlp(&account_db, &mut used_code)?; let fat_rlp = account::to_fat_rlp(&account, &account_db, &mut used_code)?;
chunker.push(account_key, fat_rlp)?; chunker.push(account_key, fat_rlp)?;
} }
@ -507,10 +506,10 @@ fn rebuild_accounts(
// fill out the storage trie and code while decoding. // fill out the storage trie and code while decoding.
let (acc, maybe_code) = { let (acc, maybe_code) = {
let mut acct_db = AccountDBMut::from_hash(db, hash); let mut acct_db = AccountDBMut::from_hash(db, hash);
Account::from_fat_rlp(&mut acct_db, fat_rlp)? account::from_fat_rlp(&mut acct_db, fat_rlp)?
}; };
let code_hash = acc.code_hash().clone(); let code_hash = acc.code_hash.clone();
match maybe_code { match maybe_code {
// new inline code // new inline code
Some(code) => status.new_code.push((code_hash, code, hash)), Some(code) => 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); *out = (hash, thin_rlp);

View File

@ -17,9 +17,9 @@
//! Snapshot test helpers. These are used to build blockchains and state tries //! Snapshot test helpers. These are used to build blockchains and state tries
//! which can be queried before and after a full snapshot/restore cycle. //! which can be queried before and after a full snapshot/restore cycle.
use basic_account::BasicAccount;
use account_db::AccountDBMut; use account_db::AccountDBMut;
use rand::Rng; use rand::Rng;
use snapshot::account::Account;
use util::DBValue; use util::DBValue;
use util::hash::{FixedHash, H256}; use util::hash::{FixedHash, H256};
@ -64,10 +64,10 @@ impl StateProducer {
// sweep once to alter storage tries. // sweep once to alter storage tries.
for &mut (ref mut address_hash, ref mut account_data) in &mut accounts_to_modify { 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); let acct_db = AccountDBMut::from_hash(db, *address_hash);
fill_storage(acct_db, account.storage_root_mut(), &mut self.storage_seed); fill_storage(acct_db, &mut account.storage_root, &mut self.storage_seed);
*account_data = DBValue::from_vec(account.to_thin_rlp()); *account_data = DBValue::from_vec(::rlp::encode(&account).to_vec());
} }
// sweep again to alter account trie. // sweep again to alter account trie.

View File

@ -16,8 +16,9 @@
//! State snapshotting tests. //! State snapshotting tests.
use basic_account::BasicAccount;
use snapshot::account;
use snapshot::{chunk_state, Error as SnapshotError, Progress, StateRebuilder}; use snapshot::{chunk_state, Error as SnapshotError, Progress, StateRebuilder};
use snapshot::account::Account;
use snapshot::io::{PackedReader, PackedWriter, SnapshotReader, SnapshotWriter}; use snapshot::io::{PackedReader, PackedWriter, SnapshotReader, SnapshotWriter};
use super::helpers::{compare_dbs, StateProducer}; use super::helpers::{compare_dbs, StateProducer};
@ -113,22 +114,21 @@ fn get_code_from_prev_chunk() {
// first one will have code inlined, // first one will have code inlined,
// second will just have its hash. // second will just have its hash.
let thin_rlp = acc_stream.out(); let thin_rlp = acc_stream.out();
let acc1 = Account::from_thin_rlp(&thin_rlp); let acc: BasicAccount = ::rlp::decode(&thin_rlp);
let acc2 = Account::from_thin_rlp(&thin_rlp);
let mut make_chunk = |acc: Account, hash| { let mut make_chunk = |acc, hash| {
let mut db = MemoryDB::new(); let mut db = MemoryDB::new();
AccountDBMut::from_hash(&mut db, hash).insert(&code[..]); 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); let mut stream = RlpStream::new_list(1);
stream.begin_list(2).append(&hash).append_raw(&fat_rlp, 1); stream.begin_list(2).append(&hash).append_raw(&fat_rlp, 1);
stream.out() stream.out()
}; };
let chunk1 = make_chunk(acc1, h1); let chunk1 = make_chunk(acc.clone(), h1);
let chunk2 = make_chunk(acc2, h2); let chunk2 = make_chunk(acc, h2);
let db_path = RandomTempPath::create_dir(); let db_path = RandomTempPath::create_dir();
let db_cfg = DatabaseConfig::with_columns(::db::NUM_COLUMNS); let db_cfg = DatabaseConfig::with_columns(::db::NUM_COLUMNS);

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
use util::{Address, H256, Uint, U256}; use util::{Address, H256, Uint, U256, FixedHash};
use util::sha3::SHA3_NULL_RLP; use util::sha3::SHA3_NULL_RLP;
use ethjson; use ethjson;
use super::seal::Seal; use super::seal::Seal;
@ -50,9 +50,9 @@ impl From<ethjson::spec::Genesis> for Genesis {
Genesis { Genesis {
seal: From::from(g.seal), seal: From::from(g.seal),
difficulty: g.difficulty.into(), difficulty: g.difficulty.into(),
author: g.author.into(), author: g.author.map_or_else(Address::zero, Into::into),
timestamp: g.timestamp.into(), timestamp: g.timestamp.map_or(0, Into::into),
parent_hash: g.parent_hash.into(), parent_hash: g.parent_hash.map_or_else(H256::zero, Into::into),
gas_limit: g.gas_limit.into(), gas_limit: g.gas_limit.into(),
transactions_root: g.transactions_root.map_or_else(|| SHA3_NULL_RLP.clone(), Into::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), receipts_root: g.receipts_root.map_or_else(|| SHA3_NULL_RLP.clone(), Into::into),

View File

@ -58,7 +58,7 @@ pub struct CommonParams {
impl From<ethjson::spec::Params> for CommonParams { impl From<ethjson::spec::Params> for CommonParams {
fn from(p: ethjson::spec::Params) -> Self { fn from(p: ethjson::spec::Params) -> Self {
CommonParams { 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(), maximum_extra_data_size: p.maximum_extra_data_size.into(),
network_id: p.network_id.into(), network_id: p.network_id.into(),
chain_id: if let Some(n) = p.chain_id { n.into() } else { p.network_id.into() }, chain_id: if let Some(n) = p.chain_id { n.into() } else { p.network_id.into() },

View File

@ -20,6 +20,7 @@ use util::*;
use pod_account::*; use pod_account::*;
use rlp::*; use rlp::*;
use lru_cache::LruCache; use lru_cache::LruCache;
use basic_account::BasicAccount;
use std::cell::{RefCell, Cell}; use std::cell::{RefCell, Cell};
@ -53,6 +54,23 @@ pub struct Account {
address_hash: Cell<Option<H256>>, address_hash: Cell<Option<H256>>,
} }
impl From<BasicAccount> 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 { impl Account {
#[cfg(test)] #[cfg(test)]
/// General constructor. /// General constructor.
@ -109,19 +127,8 @@ impl Account {
/// Create a new account from RLP. /// Create a new account from RLP.
pub fn from_rlp(rlp: &[u8]) -> Account { pub fn from_rlp(rlp: &[u8]) -> Account {
let r: Rlp = Rlp::new(rlp); let basic: BasicAccount = ::rlp::decode(rlp);
Account { basic.into()
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),
}
} }
/// Create a new contract account. /// Create a new contract account.

View File

@ -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 <http://www.gnu.org/licenses/>.
//! 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<D>(decoder: &D) -> Result<Self, DecoderError> 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)?,
})
}
}

View File

@ -37,3 +37,4 @@ pub mod mode;
pub mod pruning_info; pub mod pruning_info;
pub mod security_level; pub mod security_level;
pub mod encoded; pub mod encoded;
pub mod basic_account;

View File

@ -1,6 +1,6 @@
{ {
"name": "parity.js", "name": "parity.js",
"version": "0.2.165", "version": "0.2.168",
"main": "release/index.js", "main": "release/index.js",
"jsnext:main": "src/index.js", "jsnext:main": "src/index.js",
"author": "Parity Team <admin@parity.io>", "author": "Parity Team <admin@parity.io>",
@ -43,8 +43,8 @@
"lint:css": "stylelint ./src/**/*.css", "lint:css": "stylelint ./src/**/*.css",
"lint:js": "eslint --ignore-path .gitignore ./src/", "lint:js": "eslint --ignore-path .gitignore ./src/",
"lint:js:cached": "eslint --cache --ignore-path .gitignore ./src/", "lint:js:cached": "eslint --cache --ignore-path .gitignore ./src/",
"test": "NODE_ENV=test mocha 'src/**/*.spec.js'", "test": "NODE_ENV=test mocha --compilers ejs:ejsify 'src/**/*.spec.js'",
"test:coverage": "NODE_ENV=test istanbul cover _mocha -- '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:e2e": "NODE_ENV=test mocha 'src/**/*.e2e.js'",
"test:npm": "(cd .npmjs && npm i) && node test/npmParity && (rm -rf .npmjs/node_modules)", "test:npm": "(cd .npmjs && npm i) && node test/npmParity && (rm -rf .npmjs/node_modules)",
"prepush": "npm run lint:cached" "prepush": "npm run lint:cached"
@ -80,6 +80,7 @@
"coveralls": "2.11.15", "coveralls": "2.11.15",
"css-loader": "0.26.1", "css-loader": "0.26.1",
"ejs-loader": "0.3.0", "ejs-loader": "0.3.0",
"ejsify": "1.0.0",
"enzyme": "2.7.0", "enzyme": "2.7.0",
"eslint": "3.11.1", "eslint": "3.11.1",
"eslint-config-semistandard": "7.0.0", "eslint-config-semistandard": "7.0.0",
@ -138,6 +139,7 @@
"blockies": "0.0.2", "blockies": "0.0.2",
"brace": "0.9.0", "brace": "0.9.0",
"bytes": "2.4.0", "bytes": "2.4.0",
"crypto-js": "3.1.9-1",
"debounce": "1.0.0", "debounce": "1.0.0",
"es6-error": "4.0.0", "es6-error": "4.0.0",
"es6-promise": "4.0.5", "es6-promise": "4.0.5",

View File

@ -49,17 +49,17 @@ function transactions (address, page, test = false) {
// page offset from 0 // page offset from 0
return _call('txlist', { return _call('txlist', {
address: address, address: address,
page: (page || 0) + 1,
offset: PAGE_SIZE, offset: PAGE_SIZE,
page: (page || 0) + 1,
sort: 'desc' sort: 'desc'
}, test).then((transactions) => { }, test).then((transactions) => {
return transactions.map((tx) => { return transactions.map((tx) => {
return { return {
blockNumber: new BigNumber(tx.blockNumber || 0),
from: util.toChecksumAddress(tx.from), from: util.toChecksumAddress(tx.from),
to: util.toChecksumAddress(tx.to),
hash: tx.hash, hash: tx.hash,
blockNumber: new BigNumber(tx.blockNumber),
timeStamp: tx.timeStamp, timeStamp: tx.timeStamp,
to: util.toChecksumAddress(tx.to),
value: tx.value value: tx.value
}; };
}); });

View File

@ -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 <http://www.gnu.org/licenses/>.
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
};

View File

@ -16,16 +16,10 @@
const nock = require('nock'); const nock = require('nock');
const ShapeShift = require('./');
const initShapeshift = (ShapeShift.default || ShapeShift);
const APIKEY = '0x123454321'; const APIKEY = '0x123454321';
const shapeshift = initShapeshift(APIKEY); function mockget (shapeshift, requests) {
const rpc = shapeshift.getRpc(); let scope = nock(shapeshift.getRpc().ENDPOINT);
function mockget (requests) {
let scope = nock(rpc.ENDPOINT);
requests.forEach((request) => { requests.forEach((request) => {
scope = scope scope = scope
@ -38,8 +32,8 @@ function mockget (requests) {
return scope; return scope;
} }
function mockpost (requests) { function mockpost (shapeshift, requests) {
let scope = nock(rpc.ENDPOINT); let scope = nock(shapeshift.getRpc().ENDPOINT);
requests.forEach((request) => { requests.forEach((request) => {
scope = scope scope = scope
@ -58,7 +52,5 @@ function mockpost (requests) {
module.exports = { module.exports = {
APIKEY, APIKEY,
mockget, mockget,
mockpost, mockpost
shapeshift,
rpc
}; };

View File

@ -16,12 +16,21 @@
const helpers = require('./helpers.spec.js'); const helpers = require('./helpers.spec.js');
const APIKEY = helpers.APIKEY; const ShapeShift = require('./');
const initShapeshift = (ShapeShift.default || ShapeShift);
const mockget = helpers.mockget; const mockget = helpers.mockget;
const mockpost = helpers.mockpost; const mockpost = helpers.mockpost;
const rpc = helpers.rpc;
describe('shapeshift/rpc', () => { describe('shapeshift/rpc', () => {
let rpc;
let shapeshift;
beforeEach(() => {
shapeshift = initShapeshift(helpers.APIKEY);
rpc = shapeshift.getRpc();
});
describe('GET', () => { describe('GET', () => {
const REPLY = { test: 'this is some result' }; const REPLY = { test: 'this is some result' };
@ -29,7 +38,7 @@ describe('shapeshift/rpc', () => {
let result; let result;
beforeEach(() => { beforeEach(() => {
scope = mockget([{ path: 'test', reply: REPLY }]); scope = mockget(shapeshift, [{ path: 'test', reply: REPLY }]);
return rpc return rpc
.get('test') .get('test')
@ -54,7 +63,7 @@ describe('shapeshift/rpc', () => {
let result; let result;
beforeEach(() => { beforeEach(() => {
scope = mockpost([{ path: 'test', reply: REPLY }]); scope = mockpost(shapeshift, [{ path: 'test', reply: REPLY }]);
return rpc return rpc
.post('test', { input: 'stuff' }) .post('test', { input: 'stuff' })
@ -76,7 +85,7 @@ describe('shapeshift/rpc', () => {
}); });
it('passes the apikey specified', () => { it('passes the apikey specified', () => {
expect(scope.body.test.apiKey).to.equal(APIKEY); expect(scope.body.test.apiKey).to.equal(helpers.APIKEY);
}); });
}); });
}); });

View File

@ -15,8 +15,9 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default function (rpc) { export default function (rpc) {
let subscriptions = []; let _subscriptions = [];
let pollStatusIntervalId = null; let _pollStatusIntervalId = null;
let _subscriptionPromises = null;
function getCoins () { function getCoins () {
return rpc.get('getcoins'); return rpc.get('getcoins');
@ -36,75 +37,93 @@ export default function (rpc) {
function shift (toAddress, returnAddress, pair) { function shift (toAddress, returnAddress, pair) {
return rpc.post('shift', { return rpc.post('shift', {
withdrawal: toAddress, pair,
pair: pair, returnAddress,
returnAddress: returnAddress withdrawal: toAddress
}); });
} }
function subscribe (depositAddress, callback) { function subscribe (depositAddress, callback) {
const idx = subscriptions.length; if (!depositAddress || !callback) {
return;
}
subscriptions.push({ const index = _subscriptions.length;
depositAddress,
_subscriptions.push({
callback, callback,
idx depositAddress,
index
}); });
// Only poll if there are subscriptions... if (_pollStatusIntervalId === null) {
if (!pollStatusIntervalId) { _pollStatusIntervalId = setInterval(_pollStatus, 2000);
pollStatusIntervalId = setInterval(_pollStatus, 2000);
} }
} }
function unsubscribe (depositAddress) { function unsubscribe (depositAddress) {
const newSubscriptions = [] _subscriptions = _subscriptions.filter((sub) => sub.depositAddress !== depositAddress);
.concat(subscriptions)
.filter((sub) => sub.depositAddress !== depositAddress);
subscriptions = newSubscriptions; if (_subscriptions.length === 0) {
clearInterval(_pollStatusIntervalId);
if (subscriptions.length === 0) { _pollStatusIntervalId = null;
clearInterval(pollStatusIntervalId);
pollStatusIntervalId = null;
} }
return true;
} }
function _getSubscriptionStatus (subscription) { function _getSubscriptionStatus (subscription) {
if (!subscription) { if (!subscription) {
return; return Promise.resolve();
} }
getStatus(subscription.depositAddress) return getStatus(subscription.depositAddress)
.then((result) => { .then((result) => {
switch (result.status) { switch (result.status) {
case 'no_deposits': case 'no_deposits':
case 'received': case 'received':
subscription.callback(null, result); subscription.callback(null, result);
return; return true;
case 'complete': case 'complete':
subscription.callback(null, result); subscription.callback(null, result);
subscriptions[subscription.idx] = null; unsubscribe(subscription.depositAddress);
return; return true;
case 'failed': case 'failed':
subscription.callback({ subscription.callback({
message: status.error, message: status.error,
fatal: true fatal: true
}); });
subscriptions[subscription.idx] = null; unsubscribe(subscription.depositAddress);
return; return true;
} }
}) })
.catch(subscription.callback); .catch(() => {
return true;
});
} }
function _pollStatus () { 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 { return {
_getSubscriptions,
_getSubscriptionPromises,
_isPolling,
getCoins, getCoins,
getMarketInfo, getMarketInfo,
getRpc, getRpc,

View File

@ -14,13 +14,29 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
const sinon = require('sinon');
const ShapeShift = require('./');
const initShapeshift = (ShapeShift.default || ShapeShift);
const helpers = require('./helpers.spec.js'); const helpers = require('./helpers.spec.js');
const mockget = helpers.mockget; const mockget = helpers.mockget;
const mockpost = helpers.mockpost; const mockpost = helpers.mockpost;
const shapeshift = helpers.shapeshift;
describe('shapeshift/calls', () => { describe('shapeshift/calls', () => {
let clock;
let shapeshift;
beforeEach(() => {
clock = sinon.useFakeTimers();
shapeshift = initShapeshift(helpers.APIKEY);
});
afterEach(() => {
clock.restore();
});
describe('getCoins', () => { describe('getCoins', () => {
const REPLY = { const REPLY = {
BTC: { BTC: {
@ -39,8 +55,8 @@ describe('shapeshift/calls', () => {
let scope; let scope;
before(() => { beforeEach(() => {
scope = mockget([{ path: 'getcoins', reply: REPLY }]); scope = mockget(shapeshift, [{ path: 'getcoins', reply: REPLY }]);
return shapeshift.getCoins(); return shapeshift.getCoins();
}); });
@ -61,8 +77,8 @@ describe('shapeshift/calls', () => {
let scope; let scope;
before(() => { beforeEach(() => {
scope = mockget([{ path: 'marketinfo/btc_ltc', reply: REPLY }]); scope = mockget(shapeshift, [{ path: 'marketinfo/btc_ltc', reply: REPLY }]);
return shapeshift.getMarketInfo('btc_ltc'); return shapeshift.getMarketInfo('btc_ltc');
}); });
@ -80,8 +96,8 @@ describe('shapeshift/calls', () => {
let scope; let scope;
before(() => { beforeEach(() => {
scope = mockget([{ path: 'txStat/0x123', reply: REPLY }]); scope = mockget(shapeshift, [{ path: 'txStat/0x123', reply: REPLY }]);
return shapeshift.getStatus('0x123'); return shapeshift.getStatus('0x123');
}); });
@ -101,8 +117,8 @@ describe('shapeshift/calls', () => {
let scope; let scope;
before(() => { beforeEach(() => {
scope = mockpost([{ path: 'shift', reply: REPLY }]); scope = mockpost(shapeshift, [{ path: 'shift', reply: REPLY }]);
return shapeshift.shift('0x456', '1BTC', 'btc_eth'); 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);
});
});
});
}); });

View File

@ -31,6 +31,12 @@ export default class Param {
} }
static toParams (params) { 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);
});
} }
} }

View File

@ -34,5 +34,14 @@ describe('abi/spec/Param', () => {
expect(params[0].name).to.equal('foo'); expect(params[0].name).to.equal('foo');
expect(params[0].kind.type).to.equal('uint'); 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');
});
}); });
}); });

View File

@ -16,7 +16,6 @@
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import sinon from 'sinon'; import sinon from 'sinon';
import 'sinon-as-promised';
import Eth from './eth'; import Eth from './eth';

View File

@ -15,7 +15,6 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon'; import sinon from 'sinon';
import 'sinon-as-promised';
import Personal from './personal'; import Personal from './personal';

View File

@ -26,7 +26,9 @@ export function decodeCallData (data) {
if (data.substr(0, 2) === '0x') { if (data.substr(0, 2) === '0x') {
return decodeCallData(data.slice(2)); 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'); throw new Error('Input to decodeCallData should be method signature + data');
} }
@ -42,10 +44,14 @@ export function decodeCallData (data) {
export function decodeMethodInput (methodAbi, paramdata) { export function decodeMethodInput (methodAbi, paramdata) {
if (!methodAbi) { if (!methodAbi) {
throw new Error('decodeMethodInput should receive valid method-specific ABI'); throw new Error('decodeMethodInput should receive valid method-specific ABI');
} else if (paramdata && paramdata.length) { }
if (paramdata && paramdata.length) {
if (!isHex(paramdata)) { if (!isHex(paramdata)) {
throw new Error('Input to decodeMethodInput should be a hex value'); 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)); return decodeMethodInput(methodAbi, paramdata.slice(2));
} }
} }

View File

@ -21,7 +21,7 @@ const TEST_ENV = process.env.NODE_ENV === 'test';
export function createIdentityImg (address, scale = 8) { export function createIdentityImg (address, scale = 8) {
return TEST_ENV return TEST_ENV
? '' ? 'test-createIdentityImg'
: blockies({ : blockies({
seed: (address || '').toLowerCase(), seed: (address || '').toLowerCase(),
size: 8, size: 8,

View File

@ -14,8 +14,21 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
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) { export function sha3 (value, options) {
return `0x${keccak_256(value)}`; 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}`;
} }

View File

@ -34,3 +34,16 @@ ReactDOM.render(
</Provider>, </Provider>,
document.querySelector('#container') document.querySelector('#container')
); );
if (module.hot) {
module.hot.accept('./registry/Container', () => {
require('./registry/Container');
ReactDOM.render(
<Provider store={ store }>
<Container />
</Provider>,
document.querySelector('#container')
);
});
}

View File

@ -44,8 +44,8 @@ export default class Application extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
contract: nullableProptype(PropTypes.object).isRequired, contract: nullableProptype(PropTypes.object.isRequired),
fee: nullableProptype(PropTypes.object).isRequired fee: nullableProptype(PropTypes.object.isRequired)
}; };
render () { render () {

View File

@ -15,11 +15,13 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { sha3 } from '../parity.js'; import { sha3 } from '../parity.js';
import { getOwner } from '../util/registry';
export const clear = () => ({ type: 'lookup clear' }); export const clear = () => ({ type: 'lookup clear' });
export const lookupStart = (name, key) => ({ type: 'lookup start', name, key }); export const lookupStart = (name, key) => ({ type: 'lookup start', name, key });
export const reverseLookupStart = (address) => ({ type: 'reverseLookup start', address }); 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 }); 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(); const { contract } = getState();
if (!contract) { if (!contract) {
return; return;
} }
const reverse = contract.functions dispatch(reverseLookupStart(lookupAddress));
.find((f) => f.name === 'reverse');
dispatch(reverseLookupStart(address)); contract.instance
.reverse
reverse.call({}, [ address ]) .call({}, [ lookupAddress ])
.then((address) => dispatch(success('reverseLookup', address))) .then((address) => {
dispatch(success('reverseLookup', address));
})
.catch((err) => { .catch((err) => {
console.error(`could not lookup reverse for ${address}`); console.error(`could not lookup reverse for ${lookupAddress}`);
if (err) { if (err) {
console.error(err.stack); console.error(err.stack);
} }
dispatch(fail('reverseLookup')); 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'));
});
};

View File

@ -23,13 +23,14 @@ import DropDownMenu from 'material-ui/DropDownMenu';
import MenuItem from 'material-ui/MenuItem'; import MenuItem from 'material-ui/MenuItem';
import RaisedButton from 'material-ui/RaisedButton'; import RaisedButton from 'material-ui/RaisedButton';
import SearchIcon from 'material-ui/svg-icons/action/search'; import SearchIcon from 'material-ui/svg-icons/action/search';
import keycode from 'keycode';
import { nullableProptype } from '~/util/proptypes'; import { nullableProptype } from '~/util/proptypes';
import Address from '../ui/address.js'; import Address from '../ui/address.js';
import renderImage from '../ui/image.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'; import styles from './lookup.css';
class Lookup extends Component { class Lookup extends Component {
@ -39,6 +40,7 @@ class Lookup extends Component {
clear: PropTypes.func.isRequired, clear: PropTypes.func.isRequired,
lookup: PropTypes.func.isRequired, lookup: PropTypes.func.isRequired,
ownerLookup: PropTypes.func.isRequired,
reverseLookup: PropTypes.func.isRequired reverseLookup: PropTypes.func.isRequired
} }
@ -50,33 +52,6 @@ class Lookup extends Component {
const { input, type } = this.state; const { input, type } = this.state;
const { result } = this.props; const { result } = this.props;
let output = '';
if (result) {
if (type === 'A') {
output = (
<code>
<Address
address={ result }
shortenHash={ false }
/>
</code>
);
} else if (type === 'IMG') {
output = renderImage(result);
} else if (type === 'CONTENT') {
output = (
<div>
<code>{ result }</code>
<p>Keep in mind that this is most likely the hash of the content you are looking for.</p>
</div>
);
} else {
output = (
<code>{ result }</code>
);
}
}
return ( return (
<Card className={ styles.lookup }> <Card className={ styles.lookup }>
<CardHeader title={ 'Query the Registry' } /> <CardHeader title={ 'Query the Registry' } />
@ -85,6 +60,7 @@ class Lookup extends Component {
hintText={ type === 'reverse' ? 'address' : 'name' } hintText={ type === 'reverse' ? 'address' : 'name' }
value={ input } value={ input }
onChange={ this.onInputChange } onChange={ this.onInputChange }
onKeyDown={ this.onKeyDown }
/> />
<DropDownMenu <DropDownMenu
value={ type } value={ type }
@ -94,6 +70,7 @@ class Lookup extends Component {
<MenuItem value='IMG' primaryText='IMG  hash of a picture in the blockchain' /> <MenuItem value='IMG' primaryText='IMG  hash of a picture in the blockchain' />
<MenuItem value='CONTENT' primaryText='CONTENT  hash of a data in the blockchain' /> <MenuItem value='CONTENT' primaryText='CONTENT  hash of a data in the blockchain' />
<MenuItem value='reverse' primaryText='reverse find a name for an address' /> <MenuItem value='reverse' primaryText='reverse find a name for an address' />
<MenuItem value='owner' primaryText='owner find a the owner' />
</DropDownMenu> </DropDownMenu>
<RaisedButton <RaisedButton
label='Lookup' label='Lookup'
@ -102,35 +79,102 @@ class Lookup extends Component {
onTouchTap={ this.onLookupClick } onTouchTap={ this.onLookupClick }
/> />
</div> </div>
<CardText>{ output }</CardText> <CardText>
{ this.renderOutput(type, result) }
</CardText>
</Card> </Card>
); );
} }
renderOutput (type, result) {
if (result === null) {
return null;
}
if (type === 'A') {
return (
<code>
<Address
address={ result }
shortenHash={ false }
/>
</code>
);
}
if (type === 'owner') {
if (!result) {
return (
<code>Not reserved yet</code>
);
}
return (
<code>
<Address
address={ result }
shortenHash={ false }
/>
</code>
);
}
if (type === 'IMG') {
return renderImage(result);
}
if (type === 'CONTENT') {
return (
<div>
<code>{ result }</code>
<p>Keep in mind that this is most likely the hash of the content you are looking for.</p>
</div>
);
}
return (
<code>{ result || 'No data' }</code>
);
}
onInputChange = (e) => { onInputChange = (e) => {
this.setState({ input: e.target.value }); this.setState({ input: e.target.value });
}; }
onKeyDown = (event) => {
const codeName = keycode(event);
if (codeName !== 'enter') {
return;
}
this.onLookupClick();
}
onTypeChange = (e, i, type) => { onTypeChange = (e, i, type) => {
this.setState({ type }); this.setState({ type });
this.props.clear(); this.props.clear();
}; }
onLookupClick = () => { onLookupClick = () => {
const { input, type } = this.state; const { input, type } = this.state;
if (type === 'reverse') { if (type === 'reverse') {
this.props.reverseLookup(input); return this.props.reverseLookup(input);
} else { }
this.props.lookup(input, type);
if (type === 'owner') {
return this.props.ownerLookup(input);
}
return this.props.lookup(input, type);
} }
};
} }
const mapStateToProps = (state) => state.lookup; const mapStateToProps = (state) => state.lookup;
const mapDispatchToProps = (dispatch) => const mapDispatchToProps = (dispatch) =>
bindActionCreators({ bindActionCreators({
clear, lookup, reverseLookup clear, lookup, ownerLookup, reverseLookup
}, dispatch); }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Lookup); export default connect(mapStateToProps, mapDispatchToProps)(Lookup);

View File

@ -24,7 +24,7 @@ const initialState = {
export default (state = initialState, action) => { export default (state = initialState, action) => {
const { type } = action; const { type } = action;
if (type.slice(0, 7) !== 'lookup ' && type.slice(0, 14) !== 'reverseLookup ') { if (!/^(lookup|reverseLookup|ownerLookup)/.test(type)) {
return state; return state;
} }

View File

@ -15,8 +15,13 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { sha3, api } from '../parity.js'; import { sha3, api } from '../parity.js';
import { getOwner, isOwned } from '../util/registry';
import postTx from '../util/post-tx'; import postTx from '../util/post-tx';
export const clearError = () => ({
type: 'clearError'
});
const alreadyQueued = (queue, action, name) => const alreadyQueued = (queue, action, name) =>
!!queue.find((entry) => entry.action === action && entry.name === 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 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) => { export const reserve = (name) => (dispatch, getState) => {
const state = getState(); const state = getState();
const account = state.accounts.selected; const account = state.accounts.selected;
const contract = state.contract; const contract = state.contract;
const fee = state.fee; const fee = state.fee;
if (!contract || !account) { if (!contract || !account) {
return; return;
} }
@ -40,10 +46,17 @@ export const reserve = (name) => (dispatch, getState) => {
if (alreadyQueued(state.names.queue, 'reserve', name)) { if (alreadyQueued(state.names.queue, 'reserve', name)) {
return; return;
} }
const reserve = contract.functions.find((f) => f.name === 'reserve');
dispatch(reserveStart(name)); dispatch(reserveStart(name));
return isOwned(contract, name)
.then((owned) => {
if (owned) {
throw new Error(`"${name}" has already been reserved`);
}
const { reserve } = contract.instance;
const options = { const options = {
from: account.address, from: account.address,
value: fee value: fee
@ -52,15 +65,15 @@ export const reserve = (name) => (dispatch, getState) => {
sha3(name) sha3(name)
]; ];
postTx(api, reserve, options, values) return postTx(api, reserve, options, values);
})
.then((txHash) => { .then((txHash) => {
dispatch(reserveSuccess(name)); dispatch(reserveSuccess(name));
}) })
.catch((err) => { .catch((err) => {
console.error(`could not reserve ${name}`); if (err.type !== 'REQUEST_REJECTED') {
console.error(`error rerserving ${name}`, err);
if (err) { return dispatch(reserveFail(name, err));
console.error(err.stack);
} }
dispatch(reserveFail(name)); 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 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) => { export const drop = (name) => (dispatch, getState) => {
const state = getState(); const state = getState();
const account = state.accounts.selected; const account = state.accounts.selected;
const contract = state.contract; const contract = state.contract;
if (!contract || !account) { if (!contract || !account) {
return; return;
} }
name = name.toLowerCase(); name = name.toLowerCase();
if (alreadyQueued(state.names.queue, 'drop', name)) { if (alreadyQueued(state.names.queue, 'drop', name)) {
return; return;
} }
const drop = contract.functions.find((f) => f.name === 'drop');
dispatch(dropStart(name)); dispatch(dropStart(name));
return getOwner(contract, name)
.then((owner) => {
if (owner.toLowerCase() !== account.address.toLowerCase()) {
throw new Error(`you are not the owner of "${name}"`);
}
const { drop } = contract.instance;
const options = { const options = {
from: account.address from: account.address
}; };
const values = [ const values = [
sha3(name) sha3(name)
]; ];
postTx(api, drop, options, values) return postTx(api, drop, options, values);
})
.then((txhash) => { .then((txhash) => {
dispatch(dropSuccess(name)); dispatch(dropSuccess(name));
}) })
.catch((err) => { .catch((err) => {
console.error(`could not drop ${name}`); if (err.type !== 'REQUEST_REJECTED') {
console.error(`error dropping ${name}`, err);
if (err) { return dispatch(dropFail(name, err));
console.error(err.stack);
} }
dispatch(reserveFail(name)); dispatch(dropFail(name));
}); });
}; };

View File

@ -35,7 +35,12 @@
.link { .link {
color: #00BCD4; color: #00BCD4;
text-decoration: none; text-decoration: none;
}
.link:hover { &:hover {
text-decoration: underline; text-decoration: underline;
}
}
.error {
color: red;
} }

View File

@ -24,9 +24,10 @@ import MenuItem from 'material-ui/MenuItem';
import RaisedButton from 'material-ui/RaisedButton'; import RaisedButton from 'material-ui/RaisedButton';
import CheckIcon from 'material-ui/svg-icons/navigation/check'; import CheckIcon from 'material-ui/svg-icons/navigation/check';
import { nullableProptype } from '~/util/proptypes';
import { fromWei } from '../parity.js'; import { fromWei } from '../parity.js';
import { reserve, drop } from './actions'; import { clearError, reserve, drop } from './actions';
import styles from './names.css'; import styles from './names.css';
const useSignerText = (<p>Use the <a href='/#/signer' className={ styles.link } target='_blank'>Signer</a> to authenticate the following changes.</p>); const useSignerText = (<p>Use the <a href='/#/signer' className={ styles.link } target='_blank'>Signer</a> to authenticate the following changes.</p>);
@ -78,35 +79,21 @@ const renderQueue = (queue) => {
class Names extends Component { class Names extends Component {
static propTypes = { static propTypes = {
error: nullableProptype(PropTypes.object.isRequired),
fee: PropTypes.object.isRequired, fee: PropTypes.object.isRequired,
pending: PropTypes.bool.isRequired, pending: PropTypes.bool.isRequired,
queue: PropTypes.array.isRequired, queue: PropTypes.array.isRequired,
clearError: PropTypes.func.isRequired,
reserve: PropTypes.func.isRequired, reserve: PropTypes.func.isRequired,
drop: PropTypes.func.isRequired drop: PropTypes.func.isRequired
} };
state = { state = {
action: 'reserve', action: 'reserve',
name: '' 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 () { render () {
const { action, name } = this.state; const { action, name } = this.state;
const { fee, pending, queue } = this.props; const { fee, pending, queue } = this.props;
@ -122,6 +109,7 @@ class Names extends Component {
: (<p className={ styles.noSpacing }>To drop a name, you have to be the owner.</p>) : (<p className={ styles.noSpacing }>To drop a name, you have to be the owner.</p>)
) )
} }
{ this.renderError() }
<div className={ styles.box }> <div className={ styles.box }>
<TextField <TextField
hintText='name' hintText='name'
@ -154,23 +142,50 @@ class Names extends Component {
); );
} }
renderError () {
const { error } = this.props;
if (!error) {
return null;
}
return (
<div className={ styles.error }>
<code>{ error.message }</code>
</div>
);
}
onNameChange = (e) => { onNameChange = (e) => {
this.clearError();
this.setState({ name: e.target.value }); this.setState({ name: e.target.value });
}; };
onActionChange = (e, i, action) => { onActionChange = (e, i, action) => {
this.clearError();
this.setState({ action }); this.setState({ action });
}; };
onSubmitClick = () => { onSubmitClick = () => {
const { action, name } = this.state; const { action, name } = this.state;
if (action === 'reserve') { if (action === 'reserve') {
this.props.reserve(name); return this.props.reserve(name);
} else if (action === 'drop') { }
this.props.drop(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 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); export default connect(mapStateToProps, mapDispatchToProps)(Names);

View File

@ -17,32 +17,55 @@
import { isAction, isStage, addToQueue, removeFromQueue } from '../util/actions'; import { isAction, isStage, addToQueue, removeFromQueue } from '../util/actions';
const initialState = { const initialState = {
error: null,
pending: false, pending: false,
queue: [] queue: []
}; };
export default (state = initialState, action) => { export default (state = initialState, action) => {
switch (action.type) {
case 'clearError':
return {
...state,
error: null
};
}
if (isAction('names', 'reserve', action)) { if (isAction('names', 'reserve', action)) {
if (isStage('start', action)) { if (isStage('start', action)) {
return { return {
...state, pending: true, ...state,
error: null,
pending: true,
queue: addToQueue(state.queue, 'reserve', action.name) queue: addToQueue(state.queue, 'reserve', action.name)
}; };
} else if (isStage('success', action) || isStage('fail', action)) { }
if (isStage('success', action) || isStage('fail', action)) {
return { return {
...state, pending: false, ...state,
error: action.error || null,
pending: false,
queue: removeFromQueue(state.queue, 'reserve', action.name) queue: removeFromQueue(state.queue, 'reserve', action.name)
}; };
} }
} else if (isAction('names', 'drop', action)) { }
if (isAction('names', 'drop', action)) {
if (isStage('start', action)) { if (isStage('start', action)) {
return { return {
...state, pending: true, ...state,
error: null,
pending: true,
queue: addToQueue(state.queue, 'drop', action.name) queue: addToQueue(state.queue, 'drop', action.name)
}; };
} else if (isStage('success', action) || isStage('fail', action)) { }
if (isStage('success', action) || isStage('fail', action)) {
return { return {
...state, pending: false, ...state,
error: action.error || null,
pending: false,
queue: removeFromQueue(state.queue, 'drop', action.name) queue: removeFromQueue(state.queue, 'drop', action.name)
}; };
} }

View File

@ -16,45 +16,57 @@
import { sha3, api } from '../parity.js'; import { sha3, api } from '../parity.js';
import postTx from '../util/post-tx'; 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 start = (name, key, value) => ({ type: 'records update start', name, key, value });
export const success = () => ({ type: 'records update success' }); 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) => { export const update = (name, key, value) => (dispatch, getState) => {
const state = getState(); const state = getState();
const account = state.accounts.selected; const account = state.accounts.selected;
const contract = state.contract; const contract = state.contract;
if (!contract || !account) { if (!contract || !account) {
return; return;
} }
name = name.toLowerCase(); name = name.toLowerCase();
dispatch(start(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}"`);
}
const fnName = key === 'A' ? 'setAddress' : 'set'; const fnName = key === 'A' ? 'setAddress' : 'set';
const setAddress = contract.functions.find((f) => f.name === fnName); const method = contract.instance[fnName];
dispatch(start(name, key, value));
const options = { const options = {
from: account.address from: account.address
}; };
const values = [ const values = [
sha3(name), sha3(name),
key, key,
value value
]; ];
postTx(api, setAddress, options, values) return postTx(api, method, options, values);
})
.then((txHash) => { .then((txHash) => {
dispatch(success()); dispatch(success());
}).catch((err) => { }).catch((err) => {
console.error(`could not update ${key} record of ${name}`); if (err.type !== 'REQUEST_REJECTED') {
console.error(`error updating ${name}`, err);
if (err) { return dispatch(fail(err));
console.error(err.stack);
} }
dispatch(fail()); dispatch(fail());

View File

@ -36,3 +36,7 @@
flex-grow: 0; flex-grow: 0;
flex-shrink: 0; flex-shrink: 0;
} }
.error {
color: red;
}

View File

@ -24,17 +24,20 @@ import MenuItem from 'material-ui/MenuItem';
import RaisedButton from 'material-ui/RaisedButton'; import RaisedButton from 'material-ui/RaisedButton';
import SaveIcon from 'material-ui/svg-icons/content/save'; 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'; import styles from './records.css';
class Records extends Component { class Records extends Component {
static propTypes = { static propTypes = {
error: nullableProptype(PropTypes.object.isRequired),
pending: PropTypes.bool.isRequired, pending: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
clearError: PropTypes.func.isRequired,
update: PropTypes.func.isRequired update: PropTypes.func.isRequired
} }
@ -53,6 +56,7 @@ class Records extends Component {
<p className={ styles.noSpacing }> <p className={ styles.noSpacing }>
You can only modify entries of names that you previously registered. You can only modify entries of names that you previously registered.
</p> </p>
{ this.renderError() }
<div className={ styles.box }> <div className={ styles.box }>
<TextField <TextField
hintText='name' hintText='name'
@ -88,22 +92,46 @@ class Records extends Component {
); );
} }
renderError () {
const { error } = this.props;
if (!error) {
return null;
}
return (
<div className={ styles.error }>
<code>{ error.message }</code>
</div>
);
}
onNameChange = (e) => { onNameChange = (e) => {
this.clearError();
this.setState({ name: e.target.value }); this.setState({ name: e.target.value });
}; };
onTypeChange = (e, i, type) => { onTypeChange = (e, i, type) => {
this.setState({ type }); this.setState({ type });
}; };
onValueChange = (e) => { onValueChange = (e) => {
this.setState({ value: e.target.value }); this.setState({ value: e.target.value });
}; };
onSaveClick = () => { onSaveClick = () => {
const { name, type, value } = this.state; const { name, type, value } = this.state;
this.props.update(name, type, value); this.props.update(name, type, value);
}; };
clearError = () => {
if (this.props.error) {
this.props.clearError();
}
};
} }
const mapStateToProps = (state) => state.records; const mapStateToProps = (state) => state.records;
const mapDispatchToProps = (dispatch) => bindActionCreators({ update }, dispatch); const mapDispatchToProps = (dispatch) => bindActionCreators({ clearError, update }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Records); export default connect(mapStateToProps, mapDispatchToProps)(Records);

View File

@ -17,11 +17,20 @@
import { isAction, isStage } from '../util/actions'; import { isAction, isStage } from '../util/actions';
const initialState = { const initialState = {
error: null,
pending: false, pending: false,
name: '', type: '', value: '' name: '', type: '', value: ''
}; };
export default (state = initialState, action) => { export default (state = initialState, action) => {
switch (action.type) {
case 'clearError':
return {
...state,
error: null
};
}
if (!isAction('records', 'update', action)) { if (!isAction('records', 'update', action)) {
return state; return state;
} }
@ -29,11 +38,15 @@ export default (state = initialState, action) => {
if (isStage('start', action)) { if (isStage('start', action)) {
return { return {
...state, pending: true, ...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 { return {
...state, pending: false, ...state, pending: false,
error: action.error || null,
name: initialState.name, type: initialState.type, value: initialState.value name: initialState.name, type: initialState.type, value: initialState.value
}; };
} }

View File

@ -16,44 +16,58 @@
import { api } from '../parity.js'; import { api } from '../parity.js';
import postTx from '../util/post-tx'; 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 start = (action, name, address) => ({ type: `reverse ${action} start`, name, address });
export const success = (action) => ({ type: `reverse ${action} success` }); 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) => { export const propose = (name, address) => (dispatch, getState) => {
const state = getState(); const state = getState();
const account = state.accounts.selected; const account = state.accounts.selected;
const contract = state.contract; const contract = state.contract;
if (!contract || !account) { if (!contract || !account) {
return; return;
} }
name = name.toLowerCase(); name = name.toLowerCase();
const proposeReverse = contract.functions.find((f) => f.name === 'proposeReverse');
dispatch(start('propose', name, address)); dispatch(start('propose', name, address));
return getOwner(contract, name)
.then((owner) => {
if (owner.toLowerCase() !== account.address.toLowerCase()) {
throw new Error(`you are not the owner of "${name}"`);
}
const { proposeReverse } = contract.instance;
const options = { const options = {
from: account.address from: account.address
}; };
const values = [ const values = [
name, name,
address address
]; ];
postTx(api, proposeReverse, options, values) return postTx(api, proposeReverse, options, values);
})
.then((txHash) => { .then((txHash) => {
dispatch(success('propose')); dispatch(success('propose'));
}) })
.catch((err) => { .catch((err) => {
console.error(`could not propose reverse ${name} for address ${address}`); if (err.type !== 'REQUEST_REJECTED') {
if (err) { console.error(`error proposing ${name}`, err);
console.error(err.stack); return dispatch(fail('propose', err));
} }
dispatch(fail('propose')); dispatch(fail('propose'));
}); });
}; };
@ -62,31 +76,42 @@ export const confirm = (name) => (dispatch, getState) => {
const state = getState(); const state = getState();
const account = state.accounts.selected; const account = state.accounts.selected;
const contract = state.contract; const contract = state.contract;
if (!contract || !account) { if (!contract || !account) {
return; return;
} }
name = name.toLowerCase(); name = name.toLowerCase();
const confirmReverse = contract.functions.find((f) => f.name === 'confirmReverse');
dispatch(start('confirm', name)); dispatch(start('confirm', name));
return getOwner(contract, name)
.then((owner) => {
if (owner.toLowerCase() !== account.address.toLowerCase()) {
throw new Error(`you are not the owner of "${name}"`);
}
const { confirmReverse } = contract.instance;
const options = { const options = {
from: account.address from: account.address
}; };
const values = [ const values = [
name name
]; ];
postTx(api, confirmReverse, options, values) return postTx(api, confirmReverse, options, values);
})
.then((txHash) => { .then((txHash) => {
dispatch(success('confirm')); dispatch(success('confirm'));
}) })
.catch((err) => { .catch((err) => {
console.error(`could not confirm reverse ${name}`); if (err.type !== 'REQUEST_REJECTED') {
if (err) { console.error(`error confirming ${name}`, err);
console.error(err.stack); return dispatch(fail('confirm', err));
} }
dispatch(fail('confirm')); dispatch(fail('confirm'));
}); });
}; };

View File

@ -17,24 +17,37 @@
import { isAction, isStage } from '../util/actions'; import { isAction, isStage } from '../util/actions';
const initialState = { const initialState = {
error: null,
pending: false, pending: false,
queue: [] queue: []
}; };
export default (state = initialState, action) => { export default (state = initialState, action) => {
switch (action.type) {
case 'clearError':
return {
...state,
error: null
};
}
if (isAction('reverse', 'propose', action)) { if (isAction('reverse', 'propose', action)) {
if (isStage('start', action)) { if (isStage('start', action)) {
return { return {
...state, pending: true, ...state, pending: true,
error: null,
queue: state.queue.concat({ queue: state.queue.concat({
action: 'propose', action: 'propose',
name: action.name, name: action.name,
address: action.address address: action.address
}) })
}; };
} else if (isStage('success', action) || isStage('fail', action)) { }
if (isStage('success', action) || isStage('fail', action)) {
return { return {
...state, pending: false, ...state, pending: false,
error: action.error || null,
queue: state.queue.filter((e) => queue: state.queue.filter((e) =>
e.action === 'propose' && e.action === 'propose' &&
e.name === action.name && e.name === action.name &&
@ -48,14 +61,18 @@ export default (state = initialState, action) => {
if (isStage('start', action)) { if (isStage('start', action)) {
return { return {
...state, pending: true, ...state, pending: true,
error: null,
queue: state.queue.concat({ queue: state.queue.concat({
action: 'confirm', action: 'confirm',
name: action.name name: action.name
}) })
}; };
} else if (isStage('success', action) || isStage('fail', action)) { }
if (isStage('success', action) || isStage('fail', action)) {
return { return {
...state, pending: false, ...state, pending: false,
error: action.error || null,
queue: state.queue.filter((e) => queue: state.queue.filter((e) =>
e.action === 'confirm' && e.action === 'confirm' &&
e.name === action.name e.name === action.name

View File

@ -37,3 +37,6 @@
flex-shrink: 0; flex-shrink: 0;
} }
.error {
color: red;
}

View File

@ -21,17 +21,20 @@ import {
Card, CardHeader, CardText, TextField, DropDownMenu, MenuItem, RaisedButton Card, CardHeader, CardText, TextField, DropDownMenu, MenuItem, RaisedButton
} from 'material-ui'; } from 'material-ui';
import { nullableProptype } from '~/util/proptypes';
import { AddIcon, CheckIcon } from '~/ui/Icons'; import { AddIcon, CheckIcon } from '~/ui/Icons';
import { propose, confirm } from './actions'; import { clearError, confirm, propose } from './actions';
import styles from './reverse.css'; import styles from './reverse.css';
class Reverse extends Component { class Reverse extends Component {
static propTypes = { static propTypes = {
error: nullableProptype(PropTypes.object.isRequired),
pending: PropTypes.bool.isRequired, pending: PropTypes.bool.isRequired,
queue: PropTypes.array.isRequired, queue: PropTypes.array.isRequired,
propose: PropTypes.func.isRequired, clearError: PropTypes.func.isRequired,
confirm: PropTypes.func.isRequired confirm: PropTypes.func.isRequired,
propose: PropTypes.func.isRequired
} }
state = { state = {
@ -77,6 +80,7 @@ class Reverse extends Component {
</strong> </strong>
</p> </p>
{ explanation } { explanation }
{ this.renderError() }
<div className={ styles.box }> <div className={ styles.box }>
<DropDownMenu <DropDownMenu
disabled={ pending } disabled={ pending }
@ -108,6 +112,20 @@ class Reverse extends Component {
); );
} }
renderError () {
const { error } = this.props;
if (!error) {
return null;
}
return (
<div className={ styles.error }>
<code>{ error.message }</code>
</div>
);
}
onNameChange = (e) => { onNameChange = (e) => {
this.setState({ name: e.target.value }); this.setState({ name: e.target.value });
}; };
@ -129,9 +147,15 @@ class Reverse extends Component {
this.props.confirm(name); this.props.confirm(name);
} }
}; };
clearError = () => {
if (this.props.error) {
this.props.clearError();
}
};
} }
const mapStateToProps = (state) => state.reverse; 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); export default connect(mapStateToProps, mapDispatchToProps)(Reverse);

View File

@ -20,31 +20,48 @@ import { connect } from 'react-redux';
import Hash from './hash'; import Hash from './hash';
import etherscanUrl from '../util/etherscan-url'; import etherscanUrl from '../util/etherscan-url';
import IdentityIcon from '../IdentityIcon'; import IdentityIcon from '../IdentityIcon';
import { nullableProptype } from '~/util/proptypes';
import styles from './address.css'; import styles from './address.css';
class Address extends Component { class Address extends Component {
static propTypes = { static propTypes = {
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
accounts: PropTypes.object.isRequired, account: nullableProptype(PropTypes.object.isRequired),
contacts: PropTypes.object.isRequired,
isTestnet: PropTypes.bool.isRequired, isTestnet: PropTypes.bool.isRequired,
key: PropTypes.string, key: PropTypes.string,
shortenHash: PropTypes.bool shortenHash: PropTypes.bool
} };
static defaultProps = { static defaultProps = {
key: 'address', key: 'address',
shortenHash: true shortenHash: true
} };
render () { render () {
const { address, accounts, contacts, isTestnet, key, shortenHash } = this.props; const { address, key } = this.props;
let caption; return (
if (accounts[address] || contacts[address]) { <div
const name = (accounts[address] || contacts[address] || {}).name; key={ key }
caption = ( className={ styles.container }
>
<IdentityIcon
address={ address }
className={ styles.align }
/>
{ this.renderCaption() }
</div>
);
}
renderCaption () {
const { address, account, isTestnet, shortenHash } = this.props;
if (account) {
const { name } = account;
return (
<a <a
className={ styles.link } className={ styles.link }
href={ etherscanUrl(address, isTestnet) } href={ etherscanUrl(address, isTestnet) }
@ -58,8 +75,9 @@ class Address extends Component {
</abbr> </abbr>
</a> </a>
); );
} else { }
caption = (
return (
<code className={ styles.align }> <code className={ styles.align }>
{ shortenHash ? ( { shortenHash ? (
<Hash <Hash
@ -70,29 +88,33 @@ class Address extends Component {
</code> </code>
); );
} }
}
return ( function mapStateToProps (initState, initProps) {
<div const { accounts, contacts } = initState;
key={ key }
className={ styles.container } const allAccounts = Object.assign({}, accounts.all, contacts);
>
<IdentityIcon // Add lower case addresses to map
address={ address } Object
className={ styles.align } .keys(allAccounts)
/> .forEach((address) => {
{ caption } allAccounts[address.toLowerCase()] = allAccounts[address];
</div> });
);
} return (state, props) => {
const { isTestnet } = state;
const { address = '' } = props;
const account = allAccounts[address] || null;
return {
account,
isTestnet
};
};
} }
export default connect( export default connect(
// mapStateToProps mapStateToProps
(state) => ({
accounts: state.accounts.all,
contacts: state.contacts,
isTestnet: state.isTestnet
}),
// mapDispatchToProps
null
)(Address); )(Address);

View File

@ -23,10 +23,20 @@ const styles = {
border: '1px solid #777' border: '1px solid #777'
}; };
export default (address) => ( export default (address) => {
if (!address || /^(0x)?0*$/.test(address)) {
return (
<code>
No image
</code>
);
}
return (
<img <img
src={ `${parityNode}/${address}/` } src={ `${parityNode}/${address}/` }
alt={ address } alt={ address }
style={ styles } style={ styles }
/> />
); );
};

View File

@ -19,7 +19,7 @@ export const isAction = (ns, type, action) => {
}; };
export const isStage = (stage, 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) => { export const addToQueue = (queue, action, name) => {
@ -27,5 +27,5 @@ export const addToQueue = (queue, action, name) => {
}; };
export const removeFromQueue = (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));
}; };

View File

@ -24,12 +24,6 @@ const postTx = (api, method, opt = {}, values = []) => {
}) })
.then((reqId) => { .then((reqId) => {
return api.pollMethod('parity_checkRequest', 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;
}); });
}; };

View File

@ -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 <http://www.gnu.org/licenses/>.
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);
};

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { showSnackbar } from './snackbarActions'; import { showSnackbar } from './snackbarActions';
import { DEFAULT_NETCHAIN } from './statusReducer';
export default class ChainMiddleware { export default class ChainMiddleware {
toMiddleware () { toMiddleware () {
@ -23,11 +24,11 @@ export default class ChainMiddleware {
const { collection } = action; const { collection } = action;
if (collection && collection.netChain) { if (collection && collection.netChain) {
const chain = collection.netChain; const newChain = collection.netChain;
const { nodeStatus } = store.getState(); const { nodeStatus } = store.getState();
if (chain !== nodeStatus.netChain) { if (newChain !== nodeStatus.netChain && nodeStatus.netChain !== DEFAULT_NETCHAIN) {
store.dispatch(showSnackbar(`Switched to ${chain}. Please reload the page.`, 5000)); store.dispatch(showSnackbar(`Switched to ${newChain}. Please reload the page.`, 60000));
} }
} }
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
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;
});
});
});

View File

@ -17,6 +17,7 @@
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { handleActions } from 'redux-actions'; import { handleActions } from 'redux-actions';
const DEFAULT_NETCHAIN = '(unknown)';
const initialState = { const initialState = {
blockNumber: new BigNumber(0), blockNumber: new BigNumber(0),
blockTimestamp: new Date(), blockTimestamp: new Date(),
@ -32,7 +33,7 @@ const initialState = {
gasLimit: new BigNumber(0), gasLimit: new BigNumber(0),
hashrate: new BigNumber(0), hashrate: new BigNumber(0),
minGasPrice: new BigNumber(0), minGasPrice: new BigNumber(0),
netChain: 'ropsten', netChain: DEFAULT_NETCHAIN,
netPeers: { netPeers: {
active: new BigNumber(0), active: new BigNumber(0),
connected: new BigNumber(0), connected: new BigNumber(0),
@ -82,3 +83,8 @@ export default handleActions({
return Object.assign({}, state, { refreshStatus }); return Object.assign({}, state, { refreshStatus });
} }
}, initialState); }, initialState);
export {
DEFAULT_NETCHAIN,
initialState
};

View File

@ -50,8 +50,7 @@ export default class Actionbar extends Component {
} }
return ( return (
<ToolbarGroup <ToolbarGroup className={ styles.toolbuttons }>
className={ styles.toolbuttons }>
{ buttons } { buttons }
</ToolbarGroup> </ToolbarGroup>
); );

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
@ -39,7 +40,12 @@ class BlockStatus extends Component {
if (!syncing) { if (!syncing) {
return ( return (
<div className={ styles.blockNumber }> <div className={ styles.blockNumber }>
{ blockNumber.toFormat() } best block <FormattedMessage
id='ui.blockStatus.bestBlock'
defaultMessage='{blockNumber} best block'
values={ {
blockNumber: blockNumber.toFormat()
} } />
</div> </div>
); );
} }
@ -47,26 +53,45 @@ class BlockStatus extends Component {
if (syncing.warpChunksAmount && syncing.warpChunksProcessed && !syncing.warpChunksAmount.eq(syncing.warpChunksProcessed)) { if (syncing.warpChunksAmount && syncing.warpChunksProcessed && !syncing.warpChunksAmount.eq(syncing.warpChunksProcessed)) {
return ( return (
<div className={ styles.syncStatus }> <div className={ styles.syncStatus }>
{ syncing.warpChunksProcessed.mul(100).div(syncing.warpChunksAmount).toFormat(2) }% warp restore <FormattedMessage
id='ui.blockStatus.warpRestore'
defaultMessage='{percentage}% warp restore'
values={ {
percentage: syncing.warpChunksProcessed.mul(100).div(syncing.warpChunksAmount).toFormat(2)
} } />
</div> </div>
); );
} }
let syncStatus = null;
let warpStatus = null; let warpStatus = null;
if (syncing.currentBlock && syncing.highestBlock) {
syncStatus = (
<span>
<FormattedMessage
id='ui.blockStatus.syncStatus'
defaultMessage='{currentBlock}/{highestBlock} syncing'
values={ {
currentBlock: syncing.currentBlock.toFormat(),
highestBlock: syncing.highestBlock.toFormat()
} } />
</span>
);
}
if (syncing.blockGap) { if (syncing.blockGap) {
const [first, last] = syncing.blockGap; const [first, last] = syncing.blockGap;
warpStatus = ( warpStatus = (
<span>, { first.mul(100).div(last).toFormat(2) }% historic</span> <span>
); <FormattedMessage
} id='ui.blockStatus.warpStatus'
defaultMessage=', {percentage}% historic'
let syncStatus = null; values={ {
percentage: first.mul(100).div(last).toFormat(2)
if (syncing && syncing.currentBlock && syncing.highestBlock) { } } />
syncStatus = ( </span>
<span>{ syncing.currentBlock.toFormat() }/{ syncing.highestBlock.toFormat() } syncing</span>
); );
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
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(
<BlockStatus { ...props } />,
{ 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');
});
});

View File

@ -19,7 +19,11 @@ import { shallow } from 'enzyme';
import Button from './button'; import Button from './button';
function renderShallow (props) { function render (props = {}) {
if (props && props.label === undefined) {
props.label = 'test';
}
return shallow( return shallow(
<Button { ...props } /> <Button { ...props } />
); );
@ -28,11 +32,11 @@ function renderShallow (props) {
describe('ui/Button', () => { describe('ui/Button', () => {
describe('rendering', () => { describe('rendering', () => {
it('renders defaults', () => { it('renders defaults', () => {
expect(renderShallow()).to.be.ok; expect(render()).to.be.ok;
}); });
it('renders with the specified className', () => { it('renders with the specified className', () => {
expect(renderShallow({ className: 'testClass' })).to.have.className('testClass'); expect(render({ className: 'testClass' })).to.have.className('testClass');
}); });
}); });
}); });

View File

@ -25,7 +25,7 @@ import styles from './certifications.css';
class Certifications extends Component { class Certifications extends Component {
static propTypes = { static propTypes = {
account: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
certifications: PropTypes.array.isRequired, certifications: PropTypes.array.isRequired,
dappsUrl: PropTypes.string.isRequired dappsUrl: PropTypes.string.isRequired
} }
@ -60,10 +60,10 @@ class Certifications extends Component {
} }
function mapStateToProps (_, initProps) { function mapStateToProps (_, initProps) {
const { account } = initProps; const { address } = initProps;
return (state) => { return (state) => {
const certifications = state.certifications[account] || []; const certifications = state.certifications[address] || [];
const dappsUrl = state.api.dappsUrl; const dappsUrl = state.api.dappsUrl;
return { certifications, dappsUrl }; return { certifications, dappsUrl };

View File

@ -15,16 +15,27 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import ActionDone from 'material-ui/svg-icons/action/done'; import { FormattedMessage } from 'react-intl';
import ContentClear from 'material-ui/svg-icons/content/clear';
import { nodeOrStringProptype } from '~/util/proptypes'; import { nodeOrStringProptype } from '~/util/proptypes';
import Button from '../Button'; import Button from '../Button';
import Modal from '../Modal'; import Modal from '../Modal';
import { CancelIcon, CheckIcon } from '../Icons';
import styles from './confirmDialog.css'; import styles from './confirmDialog.css';
const DEFAULT_NO = (
<FormattedMessage
id='ui.confirmDialog.no'
defaultMessage='no' />
);
const DEFAULT_YES = (
<FormattedMessage
id='ui.confirmDialog.yes'
defaultMessage='yes' />
);
export default class ConfirmDialog extends Component { export default class ConfirmDialog extends Component {
static propTypes = { static propTypes = {
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
@ -33,10 +44,10 @@ export default class ConfirmDialog extends Component {
iconDeny: PropTypes.node, iconDeny: PropTypes.node,
labelConfirm: PropTypes.string, labelConfirm: PropTypes.string,
labelDeny: PropTypes.string, labelDeny: PropTypes.string,
title: nodeOrStringProptype().isRequired,
visible: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired, onConfirm: PropTypes.func.isRequired,
onDeny: PropTypes.func.isRequired onDeny: PropTypes.func.isRequired,
title: nodeOrStringProptype().isRequired,
visible: PropTypes.bool.isRequired
} }
render () { render () {
@ -60,12 +71,12 @@ export default class ConfirmDialog extends Component {
return [ return [
<Button <Button
label={ labelDeny || 'no' } icon={ iconDeny || <CancelIcon /> }
icon={ iconDeny || <ContentClear /> } label={ labelDeny || DEFAULT_NO }
onClick={ onDeny } />, onClick={ onDeny } />,
<Button <Button
label={ labelConfirm || 'yes' } icon={ iconConfirm || <CheckIcon /> }
icon={ iconConfirm || <ActionDone /> } label={ labelConfirm || DEFAULT_YES }
onClick={ onConfirm } /> onClick={ onConfirm } />
]; ];
} }

View File

@ -0,0 +1,157 @@
// 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 <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React, { PropTypes } from 'react';
import sinon from 'sinon';
import muiTheme from '../Theme';
import ConfirmDialog from './';
let component;
let instance;
let onConfirm;
let onDeny;
function createRedux () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
settings: {
backgroundSeed: 'xyz'
}
};
}
};
}
function render (props = {}) {
onConfirm = sinon.stub();
onDeny = sinon.stub();
if (props.visible === undefined) {
props.visible = true;
}
const baseComponent = shallow(
<ConfirmDialog
{ ...props }
title='test title'
onConfirm={ onConfirm }
onDeny={ onDeny }>
<div id='testContent'>
some test content
</div>
</ConfirmDialog>
);
instance = baseComponent.instance();
component = baseComponent.find('Connect(Modal)').shallow({
childContextTypes: {
muiTheme: PropTypes.object,
store: PropTypes.object
},
context: {
muiTheme,
store: createRedux()
}
});
return component;
}
describe('ui/ConfirmDialog', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
it('renders the body as provided', () => {
expect(render().find('div[id="testContent"]').text()).to.equal('some test content');
});
describe('properties', () => {
let props;
beforeEach(() => {
props = render().props();
});
it('passes the actions', () => {
expect(props.actions).to.deep.equal(instance.renderActions());
});
it('passes title', () => {
expect(props.title).to.equal('test title');
});
it('passes visiblity flag', () => {
expect(props.visible).to.be.true;
});
});
describe('renderActions', () => {
describe('defaults', () => {
let buttons;
beforeEach(() => {
render();
buttons = instance.renderActions();
});
it('renders with supplied onConfim/onDeny callbacks', () => {
expect(buttons[0].props.onClick).to.deep.equal(onDeny);
expect(buttons[1].props.onClick).to.deep.equal(onConfirm);
});
it('renders default labels', () => {
expect(buttons[0].props.label.props.id).to.equal('ui.confirmDialog.no');
expect(buttons[1].props.label.props.id).to.equal('ui.confirmDialog.yes');
});
it('renders default icons', () => {
expect(buttons[0].props.icon.type.displayName).to.equal('ContentClear');
expect(buttons[1].props.icon.type.displayName).to.equal('NavigationCheck');
});
});
describe('overrides', () => {
let buttons;
beforeEach(() => {
render({
labelConfirm: 'labelConfirm',
labelDeny: 'labelDeny',
iconConfirm: 'iconConfirm',
iconDeny: 'iconDeny'
});
buttons = instance.renderActions();
});
it('renders supplied labels', () => {
expect(buttons[0].props.label).to.equal('labelDeny');
expect(buttons[1].props.label).to.equal('labelConfirm');
});
it('renders supplied icons', () => {
expect(buttons[0].props.icon).to.equal('iconDeny');
expect(buttons[1].props.icon).to.equal('iconConfirm');
});
});
});
});

View File

@ -19,7 +19,7 @@ import { shallow } from 'enzyme';
import Container from './container'; import Container from './container';
function renderShallow (props) { function render (props) {
return shallow( return shallow(
<Container { ...props } /> <Container { ...props } />
); );
@ -28,11 +28,24 @@ function renderShallow (props) {
describe('ui/Container', () => { describe('ui/Container', () => {
describe('rendering', () => { describe('rendering', () => {
it('renders defaults', () => { it('renders defaults', () => {
expect(renderShallow()).to.be.ok; expect(render()).to.be.ok;
}); });
it('renders with the specified className', () => { it('renders with the specified className', () => {
expect(renderShallow({ className: 'testClass' })).to.have.className('testClass'); expect(render({ className: 'testClass' })).to.have.className('testClass');
});
});
describe('sections', () => {
it('renders the Card', () => {
expect(render().find('Card')).to.have.length(1);
});
it('renders the Title', () => {
const title = render({ title: 'title' }).find('Title');
expect(title).to.have.length(1);
expect(title.props().title).to.equal('title');
}); });
}); });
}); });

View File

@ -60,6 +60,7 @@ class AddressSelect extends Component {
// Optional props // Optional props
allowCopy: PropTypes.bool, allowCopy: PropTypes.bool,
allowInput: PropTypes.bool, allowInput: PropTypes.bool,
className: PropTypes.string,
disabled: PropTypes.bool, disabled: PropTypes.bool,
error: nodeOrStringProptype(), error: nodeOrStringProptype(),
hint: nodeOrStringProptype(), hint: nodeOrStringProptype(),
@ -123,13 +124,14 @@ class AddressSelect extends Component {
renderInput () { renderInput () {
const { focused } = this.state; 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 = ( const input = (
<InputAddress <InputAddress
accountsInfo={ accountsInfo } accountsInfo={ accountsInfo }
allowCopy={ allowCopy } allowCopy={ allowCopy }
disabled={ disabled } className={ className }
disabled={ disabled || readOnly }
error={ error } error={ error }
hint={ hint } hint={ hint }
focused={ focused } focused={ focused }
@ -215,8 +217,9 @@ class AddressSelect extends Component {
} }
const { address, addressError } = validateAddress(inputValue); const { address, addressError } = validateAddress(inputValue);
const { registryValues } = this.store;
if (addressError) { if (addressError || registryValues.length > 0) {
return null; return null;
} }

View File

@ -22,6 +22,8 @@ import { FormattedMessage } from 'react-intl';
import Contracts from '~/contracts'; import Contracts from '~/contracts';
import { sha3 } from '~/api/util/sha3'; import { sha3 } from '~/api/util/sha3';
const ZERO = /^(0x)?0*$/;
export default class AddressSelectStore { export default class AddressSelectStore {
@observable values = []; @observable values = [];
@ -38,41 +40,75 @@ export default class AddressSelectStore {
registry registry
.getContract('emailverification') .getContract('emailverification')
.then((emailVerification) => { .then((emailVerification) => {
this.regLookups.push({ this.regLookups.push((email) => {
lookup: (value) => {
return emailVerification return emailVerification
.instance .instance
.reverse.call({}, [ sha3(value) ]); .reverse
}, .call({}, [ sha3(email) ])
describe: (value) => ( .then((address) => {
return {
address,
description: (
<FormattedMessage <FormattedMessage
id='addressSelect.fromEmail' id='addressSelect.fromEmail'
defaultMessage='Verified using email {value}' defaultMessage='Verified using email {email}'
values={ { values={ {
value email
} } } }
/> />
) )
};
});
}); });
}); });
registry registry
.getInstance() .getInstance()
.then((registryInstance) => { .then((registryInstance) => {
this.regLookups.push({ this.regLookups.push((name) => {
lookup: (value) => {
return registryInstance return registryInstance
.getAddress.call({}, [ sha3(value), 'A' ]); .getAddress
}, .call({}, [ sha3(name), 'A' ])
describe: (value) => ( .then((address) => {
return {
address,
name,
description: (
<FormattedMessage <FormattedMessage
id='addressSelect.fromRegistry' id='addressSelect.fromRegistry'
defaultMessage='{value} (from registry)' defaultMessage='{name} (from registry)'
values={ { values={ {
value name
} } } }
/> />
) )
};
});
});
this.regLookups.push((address) => {
return registryInstance
.reverse
.call({}, [ address ])
.then((name) => {
if (!name) {
return null;
}
return {
address,
name,
description: (
<FormattedMessage
id='addressSelect.fromRegistry'
defaultMessage='{name} (from registry)'
values={ {
name
} }
/>
)
};
});
}); });
}); });
} }
@ -149,32 +185,30 @@ export default class AddressSelectStore {
// Registries Lookup // Registries Lookup
this.registryValues = []; this.registryValues = [];
const lookups = this.regLookups.map((regLookup) => regLookup.lookup(value)); const lookups = this.regLookups.map((regLookup) => regLookup(value));
Promise Promise
.all(lookups) .all(lookups)
.then((results) => { .then((results) => {
return results return results
.map((result, index) => { .filter((result) => result && !ZERO.test(result.address));
if (/^(0x)?0*$/.test(result)) { })
return; .then((results) => {
} this.registryValues = results
.map((result) => {
const lowercaseResult = result.toLowerCase(); const lowercaseAddress = result.address.toLowerCase();
const account = flatMap(this.initValues, (cat) => cat.values) const account = flatMap(this.initValues, (cat) => cat.values)
.find((account) => account.address.toLowerCase() === lowercaseResult); .find((account) => account.address.toLowerCase() === lowercaseAddress);
return { if (account && account.name) {
description: this.regLookups[index].describe(value), result.name = account.name;
address: result, } else if (!result.name) {
name: account && account.name || value result.name = value;
}; }
})
.filter((data) => data); return result;
}) });
.then((registryValues) => {
this.registryValues = registryValues;
}); });
} }

View File

@ -75,6 +75,12 @@ class InputAddress extends Component {
containerClasses.push(styles.small); containerClasses.push(styles.small);
} }
const props = {};
if (!readOnly && !disabled) {
props.focused = focused;
}
return ( return (
<div className={ containerClasses.join(' ') }> <div className={ containerClasses.join(' ') }>
<Input <Input
@ -82,7 +88,6 @@ class InputAddress extends Component {
className={ classes.join(' ') } className={ classes.join(' ') }
disabled={ disabled } disabled={ disabled }
error={ error } error={ error }
focused={ focused }
hideUnderline={ hideUnderline } hideUnderline={ hideUnderline }
hint={ hint } hint={ hint }
label={ label } label={ label }
@ -96,7 +101,9 @@ class InputAddress extends Component {
text && account text && account
? account.name ? account.name
: (nullName || value) : (nullName || value)
} /> }
{ ...props }
/>
{ icon } { icon }
</div> </div>
); );

View File

@ -27,6 +27,7 @@ class InputAddressSelect extends Component {
contracts: PropTypes.object.isRequired, contracts: PropTypes.object.isRequired,
allowCopy: PropTypes.bool, allowCopy: PropTypes.bool,
className: PropTypes.string,
error: PropTypes.string, error: PropTypes.string,
hint: PropTypes.string, hint: PropTypes.string,
label: PropTypes.string, label: PropTypes.string,
@ -36,13 +37,14 @@ class InputAddressSelect extends Component {
}; };
render () { 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 ( return (
<AddressSelect <AddressSelect
allowCopy={ allowCopy } allowCopy={ allowCopy }
allowInput allowInput
accounts={ accounts } accounts={ accounts }
className={ className }
contacts={ contacts } contacts={ contacts }
contracts={ contracts } contracts={ contracts }
error={ error } error={ error }

View File

@ -41,6 +41,7 @@ export default class TypedInput extends Component {
accounts: PropTypes.object, accounts: PropTypes.object,
allowCopy: PropTypes.bool, allowCopy: PropTypes.bool,
className: PropTypes.string,
error: PropTypes.any, error: PropTypes.any,
hint: PropTypes.string, hint: PropTypes.string,
isEth: PropTypes.bool, isEth: PropTypes.bool,
@ -91,7 +92,7 @@ export default class TypedInput extends Component {
const { type } = param; const { type } = param;
if (type === ABI_TYPES.ARRAY) { if (type === ABI_TYPES.ARRAY) {
const { accounts, label, value = param.default } = this.props; const { accounts, className, label, value = param.default } = this.props;
const { subtype, length } = param; const { subtype, length } = param;
const fixedLength = !!length; const fixedLength = !!length;
@ -107,6 +108,7 @@ export default class TypedInput extends Component {
<TypedInput <TypedInput
accounts={ accounts } accounts={ accounts }
allowCopy={ allowCopy } allowCopy={ allowCopy }
className={ className }
key={ `${subtype.type}_${index}` } key={ `${subtype.type}_${index}` }
onChange={ onChange } onChange={ onChange }
param={ subtype } param={ subtype }
@ -236,17 +238,34 @@ export default class TypedInput extends Component {
); );
} }
getNumberValue (value) {
if (!value) {
return value;
}
const { readOnly } = this.props;
const rawValue = typeof value === 'string'
? value.replace(/,/g, '')
: value;
const bnValue = new BigNumber(rawValue);
return readOnly
? bnValue.toFormat()
: bnValue.toNumber();
}
renderInteger (value = this.props.value, onChange = this.onChange) { renderInteger (value = this.props.value, onChange = this.onChange) {
const { allowCopy, label, error, hint, min, max, readOnly } = this.props; const { allowCopy, className, label, error, hint, min, max, readOnly } = this.props;
const param = this.getParam(); const param = this.getParam();
const realValue = value const realValue = this.getNumberValue(value);
? (new BigNumber(value))[readOnly ? 'toFormat' : 'toNumber']()
: value;
return ( return (
<Input <Input
allowCopy={ allowCopy } allowCopy={ allowCopy }
className={ className }
label={ label } label={ label }
hint={ hint } hint={ hint }
value={ realValue } value={ realValue }
@ -269,16 +288,15 @@ export default class TypedInput extends Component {
* @see https://github.com/facebook/react/issues/1549 * @see https://github.com/facebook/react/issues/1549
*/ */
renderFloat (value = this.props.value, onChange = this.onChange) { renderFloat (value = this.props.value, onChange = this.onChange) {
const { allowCopy, label, error, hint, min, max, readOnly } = this.props; const { allowCopy, className, label, error, hint, min, max, readOnly } = this.props;
const param = this.getParam(); const param = this.getParam();
const realValue = value const realValue = this.getNumberValue(value);
? (new BigNumber(value))[readOnly ? 'toFormat' : 'toNumber']()
: value;
return ( return (
<Input <Input
allowCopy={ allowCopy } allowCopy={ allowCopy }
className={ className }
label={ label } label={ label }
hint={ hint } hint={ hint }
value={ realValue } value={ realValue }
@ -293,11 +311,12 @@ export default class TypedInput extends Component {
} }
renderDefault () { renderDefault () {
const { allowCopy, label, value, error, hint, readOnly } = this.props; const { allowCopy, className, label, value, error, hint, readOnly } = this.props;
return ( return (
<Input <Input
allowCopy={ allowCopy } allowCopy={ allowCopy }
className={ className }
label={ label } label={ label }
hint={ hint } hint={ hint }
value={ value } value={ value }
@ -309,12 +328,13 @@ export default class TypedInput extends Component {
} }
renderAddress () { renderAddress () {
const { accounts, allowCopy, label, value, error, hint, readOnly } = this.props; const { accounts, allowCopy, className, label, value, error, hint, readOnly } = this.props;
return ( return (
<InputAddressSelect <InputAddressSelect
allowCopy={ allowCopy } allowCopy={ allowCopy }
accounts={ accounts } accounts={ accounts }
className={ className }
error={ error } error={ error }
hint={ hint } hint={ hint }
label={ label } label={ label }
@ -326,7 +346,7 @@ export default class TypedInput extends Component {
} }
renderBoolean () { renderBoolean () {
const { allowCopy, label, value, error, hint, readOnly } = this.props; const { allowCopy, className, label, value, error, hint, readOnly } = this.props;
if (readOnly) { if (readOnly) {
return this.renderDefault(); return this.renderDefault();
@ -346,6 +366,7 @@ export default class TypedInput extends Component {
return ( return (
<Select <Select
allowCopy={ allowCopy } allowCopy={ allowCopy }
className={ className }
error={ error } error={ error }
hint={ hint } hint={ hint }
label={ label } label={ label }

View File

@ -29,6 +29,7 @@ const api = {
const store = { const store = {
estimated: '123', estimated: '123',
histogram: {},
priceDefault: '456', priceDefault: '456',
totalValue: '789', totalValue: '789',
setGas: sinon.stub(), setGas: sinon.stub(),

View File

@ -18,28 +18,42 @@ import AddIcon from 'material-ui/svg-icons/content/add';
import CancelIcon from 'material-ui/svg-icons/content/clear'; import CancelIcon from 'material-ui/svg-icons/content/clear';
import CheckIcon from 'material-ui/svg-icons/navigation/check'; import CheckIcon from 'material-ui/svg-icons/navigation/check';
import CloseIcon from 'material-ui/svg-icons/navigation/close'; 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 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 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 NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back'; import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
import SaveIcon from 'material-ui/svg-icons/content/save'; import SaveIcon from 'material-ui/svg-icons/content/save';
import SendIcon from 'material-ui/svg-icons/content/send'; import SendIcon from 'material-ui/svg-icons/content/send';
import SnoozeIcon from 'material-ui/svg-icons/av/snooze'; 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 VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock';
export { export {
AddIcon, AddIcon,
CancelIcon, CancelIcon,
CheckIcon, CheckIcon,
CloseIcon, CloseIcon,
CompareIcon,
ComputerIcon,
ContractIcon, ContractIcon,
DashboardIcon,
DeleteIcon,
DoneIcon, DoneIcon,
EditIcon,
LockedIcon, LockedIcon,
NextIcon, NextIcon,
PrevIcon, PrevIcon,
SaveIcon, SaveIcon,
SendIcon, SendIcon,
SnoozeIcon, SnoozeIcon,
VisibleIcon VerifyIcon,
VisibleIcon,
VpnIcon
}; };

View File

@ -34,8 +34,8 @@ class IdentityIcon extends Component {
button: PropTypes.bool, button: PropTypes.bool,
center: PropTypes.bool, center: PropTypes.bool,
className: PropTypes.string, className: PropTypes.string,
inline: PropTypes.bool,
images: PropTypes.object.isRequired, images: PropTypes.object.isRequired,
inline: PropTypes.bool,
padded: PropTypes.bool, padded: PropTypes.bool,
tiny: PropTypes.bool tiny: PropTypes.bool
} }

View File

@ -0,0 +1,120 @@
// 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 <http://www.gnu.org/licenses/>.
import { mount } from 'enzyme';
import React, { PropTypes } from 'react';
import sinon from 'sinon';
import muiTheme from '../Theme';
import IdentityIcon from './';
const ADDRESS0 = '0x0000000000000000000000000000000000000000';
const ADDRESS1 = '0x0123456789012345678901234567890123456789';
const ADDRESS2 = '0x9876543210987654321098765432109876543210';
let component;
function createApi () {
return {
dappsUrl: 'dappsUrl/'
};
}
function createRedux () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
images: {
[ADDRESS2]: 'reduxImage'
}
};
}
};
}
function render (props = {}) {
if (props && props.address === undefined) {
props.address = ADDRESS1;
}
component = mount(
<IdentityIcon { ...props } />,
{
childContextTypes: {
api: PropTypes.object,
muiTheme: PropTypes.object
},
context: {
api: createApi(),
muiTheme,
store: createRedux()
}
}
);
return component;
}
describe('ui/IdentityIcon', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
describe('images', () => {
it('renders an <img> with address specified', () => {
const img = render().find('img');
expect(img).to.have.length(1);
expect(img.props().src).to.equal('test-createIdentityImg');
});
it('renders an <img> with redux source when available', () => {
const img = render({ address: ADDRESS2 }).find('img');
expect(img).to.have.length(1);
expect(img.props().src).to.equal('dappsUrl/reduxImage');
});
it('renders an <ContractIcon> with no address specified', () => {
expect(render({ address: null }).find('ActionCode')).to.have.length(1);
});
it('renders an <CancelIcon> with 0x00..00 address specified', () => {
expect(render({ address: ADDRESS0 }).find('ContentClear')).to.have.length(1);
});
});
describe('sizes', () => {
it('renders 56px by default', () => {
expect(render().find('img').props().width).to.equal('56px');
});
it('renders 16px for tiny', () => {
expect(render({ tiny: true }).find('img').props().width).to.equal('16px');
});
it('renders 24px for button', () => {
expect(render({ button: true }).find('img').props().width).to.equal('24px');
});
it('renders 32px for inline', () => {
expect(render({ inline: true }).find('img').props().width).to.equal('32px');
});
});
});

View File

@ -15,13 +15,23 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { isNullAddress } from '~/util/validation'; import { isNullAddress } from '~/util/validation';
import ShortenedHash from '../ShortenedHash'; import ShortenedHash from '../ShortenedHash';
const defaultName = 'UNNAMED'; const defaultName = (
<FormattedMessage
id='ui.identityName.unnamed'
defaultMessage='UNNAMED' />
);
const defaultNameNull = (
<FormattedMessage
id='ui.identityName.null'
defaultMessage='NULL' />
);
class IdentityName extends Component { class IdentityName extends Component {
static propTypes = { static propTypes = {
@ -43,7 +53,7 @@ class IdentityName extends Component {
return null; return null;
} }
const nullName = isNullAddress(address) ? 'null' : null; const nullName = isNullAddress(address) ? defaultNameNull : null;
const addressFallback = nullName || (shorten ? (<ShortenedHash data={ address } />) : address); const addressFallback = nullName || (shorten ? (<ShortenedHash data={ address } />) : address);
const fallback = unknown ? defaultName : addressFallback; const fallback = unknown ? defaultName : addressFallback;
const isUuid = account && account.name === account.uuid; const isUuid = account && account.name === account.uuid;

View File

@ -14,8 +14,10 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import React from 'react';
import { IntlProvider } from 'react-intl';
import sinon from 'sinon'; import sinon from 'sinon';
import IdentityName from './identityName'; import IdentityName from './identityName';
@ -44,9 +46,11 @@ const STORE = {
function render (props) { function render (props) {
return mount( return mount(
<IntlProvider locale='en'>
<IdentityName <IdentityName
store={ STORE } store={ STORE }
{ ...props } /> { ...props } />
</IntlProvider>
); );
} }
@ -74,7 +78,7 @@ describe('ui/IdentityName', () => {
}); });
it('renders 0x000...000 as null', () => { it('renders 0x000...000 as null', () => {
expect(render({ address: ADDR_NULL }).text()).to.equal('null'); expect(render({ address: ADDR_NULL }).text()).to.equal('NULL');
}); });
}); });
}); });

View File

@ -14,12 +14,11 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import CircularProgress from 'material-ui/CircularProgress'; import CircularProgress from 'material-ui/CircularProgress';
import { Input, InputAddress } from '../Form'; import { TypedInput, InputAddress } from '../Form';
import MethodDecodingStore from './methodDecodingStore'; import MethodDecodingStore from './methodDecodingStore';
import styles from './methodDecoding.css'; import styles from './methodDecoding.css';
@ -245,6 +244,7 @@ class MethodDecoding extends Component {
renderDeploy () { renderDeploy () {
const { historic, transaction } = this.props; const { historic, transaction } = this.props;
const { methodInputs } = this.state;
if (!historic) { if (!historic) {
return ( return (
@ -261,6 +261,14 @@ class MethodDecoding extends Component {
</div> </div>
{ this.renderAddressName(transaction.creates, false) } { this.renderAddressName(transaction.creates, false) }
<div>
{ methodInputs && methodInputs.length ? 'with the following parameters:' : ''}
</div>
<div className={ styles.inputs }>
{ this.renderInputs() }
</div>
</div> </div>
); );
} }
@ -364,39 +372,31 @@ class MethodDecoding extends Component {
renderInputs () { renderInputs () {
const { methodInputs } = this.state; const { methodInputs } = this.state;
return methodInputs.map((input, index) => { if (!methodInputs || methodInputs.length === 0) {
switch (input.type) { return null;
case 'address':
return (
<InputAddress
disabled
text
key={ index }
className={ styles.input }
value={ input.value }
label={ input.type } />
);
default:
return (
<Input
readOnly
allowCopy
key={ index }
className={ styles.input }
value={ this.renderValue(input.value) }
label={ input.type } />
);
} }
const inputs = methodInputs.map((input, index) => {
return (
<TypedInput
allowCopy
className={ styles.input }
label={ input.type }
key={ index }
param={ input.type }
readOnly
value={ this.renderValue(input.value) }
/>
);
}); });
return inputs;
} }
renderValue (value) { renderValue (value) {
const { api } = this.context; const { api } = this.context;
if (api.util.isInstanceOf(value, BigNumber)) { if (api.util.isArray(value)) {
return value.toFormat(0);
} else if (api.util.isArray(value)) {
return api.util.bytesToHex(value); return api.util.bytesToHex(value);
} }

View File

@ -18,6 +18,8 @@ import Contracts from '~/contracts';
import Abi from '~/abi'; import Abi from '~/abi';
import * as abis from '~/contracts/abi'; import * as abis from '~/contracts/abi';
import { decodeMethodInput } from '~/api/util/decode';
const CONTRACT_CREATE = '0x60606040'; const CONTRACT_CREATE = '0x60606040';
let instance = null; let instance = null;
@ -26,6 +28,8 @@ export default class MethodDecodingStore {
api = null; api = null;
_bytecodes = {};
_contractsAbi = {};
_isContract = {}; _isContract = {};
_methods = {}; _methods = {};
@ -46,12 +50,17 @@ export default class MethodDecodingStore {
if (!contract || !contract.meta || !contract.meta.abi) { if (!contract || !contract.meta || !contract.meta.abi) {
return; return;
} }
this.loadFromAbi(contract.meta.abi); this.loadFromAbi(contract.meta.abi, contract.address);
}); });
} }
loadFromAbi (_abi) { loadFromAbi (_abi, contractAddress) {
const abi = new Abi(_abi); const abi = new Abi(_abi);
if (contractAddress && abi) {
this._contractsAbi[contractAddress] = abi;
}
abi abi
.functions .functions
.map((f) => ({ sign: f.signature, abi: f.abi })) .map((f) => ({ sign: f.signature, abi: f.abi }))
@ -111,6 +120,7 @@ export default class MethodDecodingStore {
const contractAddress = isReceived ? transaction.from : transaction.to; const contractAddress = isReceived ? transaction.from : transaction.to;
const input = transaction.input || transaction.data; const input = transaction.input || transaction.data;
result.input = input;
result.received = isReceived; result.received = isReceived;
// No input, should be a ETH transfer // No input, should be a ETH transfer
@ -118,17 +128,20 @@ export default class MethodDecodingStore {
return Promise.resolve(result); return Promise.resolve(result);
} }
try { let signature;
const { signature } = this.api.util.decodeCallData(input);
if (signature === CONTRACT_CREATE || transaction.creates) { try {
result.contract = true; const decodeCallDataResult = this.api.util.decodeCallData(input);
return Promise.resolve({ ...result, deploy: true }); signature = decodeCallDataResult.signature;
}
} catch (e) {} } catch (e) {}
// Contract deployment
if (!signature || signature === CONTRACT_CREATE || transaction.creates) {
return this.decodeContractCreation(result, contractAddress || transaction.creates);
}
return this return this
.isContract(contractAddress || transaction.creates) .isContract(contractAddress)
.then((isContract) => { .then((isContract) => {
result.contract = isContract; result.contract = isContract;
@ -140,11 +153,6 @@ export default class MethodDecodingStore {
result.signature = signature; result.signature = signature;
result.params = paramdata; result.params = paramdata;
// Contract deployment
if (!signature) {
return Promise.resolve({ ...result, deploy: true });
}
return this return this
.fetchMethodAbi(signature) .fetchMethodAbi(signature)
.then((abi) => { .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) { fetchMethodAbi (signature) {
if (this._methods[signature] !== undefined) { if (this._methods[signature] !== undefined) {
return Promise.resolve(this._methods[signature]); return Promise.resolve(this._methods[signature]);
@ -209,7 +279,7 @@ export default class MethodDecodingStore {
return Promise.resolve(this._isContract[contractAddress]); return Promise.resolve(this._isContract[contractAddress]);
} }
this._isContract[contractAddress] = this.api.eth this._isContract[contractAddress] = this
.getCode(contractAddress) .getCode(contractAddress)
.then((bytecode) => { .then((bytecode) => {
// Is a contract if the address contains *valid* bytecode // Is a contract if the address contains *valid* bytecode
@ -222,4 +292,24 @@ export default class MethodDecodingStore {
return Promise.resolve(this._isContract[contractAddress]); 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]);
}
} }

View File

@ -16,6 +16,7 @@
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { LinearProgress } from 'material-ui'; import { LinearProgress } from 'material-ui';
@ -33,8 +34,8 @@ class TxHash extends Component {
static propTypes = { static propTypes = {
hash: PropTypes.string.isRequired, hash: PropTypes.string.isRequired,
isTest: PropTypes.bool, isTest: PropTypes.bool,
summary: PropTypes.bool, maxConfirmations: PropTypes.number,
maxConfirmations: PropTypes.number summary: PropTypes.bool
} }
static defaultProps = { static defaultProps = {
@ -43,14 +44,14 @@ class TxHash extends Component {
state = { state = {
blockNumber: new BigNumber(0), blockNumber: new BigNumber(0),
transaction: null, subscriptionId: null,
subscriptionId: null transaction: null
} }
componentDidMount () { componentDidMount () {
const { api } = this.context; const { api } = this.context;
api.subscribe('eth_blockNumber', this.onBlockNumber).then((subscriptionId) => { return api.subscribe('eth_blockNumber', this.onBlockNumber).then((subscriptionId) => {
this.setState({ subscriptionId }); this.setState({ subscriptionId });
}); });
} }
@ -59,28 +60,28 @@ class TxHash extends Component {
const { api } = this.context; const { api } = this.context;
const { subscriptionId } = this.state; const { subscriptionId } = this.state;
api.unsubscribe(subscriptionId); return api.unsubscribe(subscriptionId);
} }
render () { render () {
const { hash, isTest, summary } = this.props; const { hash, isTest, summary } = this.props;
const link = ( const hashLink = (
<a href={ txLink(hash, isTest) } target='_blank'> <a href={ txLink(hash, isTest) } target='_blank'>
<ShortenedHash data={ hash } /> <ShortenedHash data={ hash } />
</a> </a>
); );
let header = (
<p>The transaction has been posted to the network, with a hash of { link }.</p>
);
if (summary) {
header = (<p>{ link }</p>);
}
return ( return (
<div> <div>
{ header } <p>{
summary
? hashLink
: <FormattedMessage
id='ui.txHash.posted'
defaultMessage='The transaction has been posted to the network with a hash of {hashLink}'
values={ { hashLink } } />
}</p>
{ this.renderConfirmations() } { this.renderConfirmations() }
</div> </div>
); );
@ -98,20 +99,22 @@ class TxHash extends Component {
color='white' color='white'
mode='indeterminate' mode='indeterminate'
/> />
<div className={ styles.progressinfo }>waiting for confirmations</div> <div className={ styles.progressinfo }>
<FormattedMessage
id='ui.txHash.waiting'
defaultMessage='waiting for confirmations' />
</div>
</div> </div>
); );
} }
const confirmations = blockNumber.minus(transaction.blockNumber).plus(1); const confirmations = blockNumber.minus(transaction.blockNumber).plus(1);
const value = Math.min(confirmations.toNumber(), maxConfirmations); const value = Math.min(confirmations.toNumber(), maxConfirmations);
let count;
if (confirmations.gt(maxConfirmations)) { let count = confirmations.toFormat(0);
count = confirmations.toFormat(0); if (confirmations.lte(maxConfirmations)) {
} else { count = `${count}/${maxConfirmations}`;
count = confirmations.toFormat(0) + `/${maxConfirmations}`;
} }
const unit = value === 1 ? 'confirmation' : 'confirmations';
return ( return (
<div className={ styles.confirm }> <div className={ styles.confirm }>
@ -121,10 +124,17 @@ class TxHash extends Component {
max={ maxConfirmations } max={ maxConfirmations }
value={ value } value={ value }
color='white' color='white'
mode='determinate' mode='determinate' />
/>
<div className={ styles.progressinfo }> <div className={ styles.progressinfo }>
<abbr title={ `block #${blockNumber.toFormat(0)}` }>{ count } { unit }</abbr> <abbr title={ `block #${blockNumber.toFormat(0)}` }>
<FormattedMessage
id='ui.txHash.confirmations'
defaultMessage='{count} {value, plural, one {confirmation} other {confirmations}}'
values={ {
count,
value
} } />
</abbr>
</div> </div>
</div> </div>
); );
@ -138,15 +148,17 @@ class TxHash extends Component {
return; return;
} }
this.setState({ blockNumber }); return api.eth
api.eth
.getTransactionReceipt(hash) .getTransactionReceipt(hash)
.then((transaction) => { .then((transaction) => {
this.setState({ transaction }); this.setState({
blockNumber,
transaction
});
}) })
.catch((error) => { .catch((error) => {
console.warn('onBlockNumber', error); console.warn('onBlockNumber', error);
this.setState({ blockNumber });
}); });
} }
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
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(
<TxHash
hash={ TXHASH }
{ ...props } />,
{ 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');
});
});
});
});

View File

@ -25,7 +25,7 @@ import TxRow from './txRow';
const api = new Api({ execute: sinon.stub() }); const api = new Api({ execute: sinon.stub() });
function renderShallow (props) { function render (props) {
return shallow( return shallow(
<TxRow <TxRow
{ ...props } />, { ...props } />,
@ -33,7 +33,7 @@ function renderShallow (props) {
); );
} }
describe('ui/TxRow', () => { describe('ui/TxList/TxRow', () => {
describe('rendering', () => { describe('rendering', () => {
it('renders defaults', () => { it('renders defaults', () => {
const block = { const block = {
@ -45,7 +45,7 @@ describe('ui/TxRow', () => {
value: new BigNumber(1) value: new BigNumber(1)
}; };
expect(renderShallow({ block, tx })).to.be.ok; expect(render({ address: '0x123', block, isTest: true, tx })).to.be.ok;
}); });
}); });
}); });

View File

@ -36,7 +36,7 @@ const STORE = {
} }
}; };
function renderShallow (props) { function render (props) {
return shallow( return shallow(
<TxList <TxList
store={ STORE } store={ STORE }
@ -48,7 +48,7 @@ function renderShallow (props) {
describe('ui/TxList', () => { describe('ui/TxList', () => {
describe('rendering', () => { describe('rendering', () => {
it('renders defaults', () => { it('renders defaults', () => {
expect(renderShallow()).to.be.ok; expect(render({ address: '0x123', hashes: [] })).to.be.ok;
}); });
}); });
}); });

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags } from '~/ui'; import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags } from '~/ui';
import CopyToClipboard from '~/ui/CopyToClipboard'; import CopyToClipboard from '~/ui/CopyToClipboard';
@ -26,50 +27,45 @@ export default class Header extends Component {
static propTypes = { static propTypes = {
account: PropTypes.object, account: PropTypes.object,
balance: PropTypes.object, balance: PropTypes.object,
className: PropTypes.string,
children: PropTypes.node, children: PropTypes.node,
isContract: PropTypes.bool, className: PropTypes.string,
hideName: PropTypes.bool hideName: PropTypes.bool,
isContract: PropTypes.bool
}; };
static defaultProps = { static defaultProps = {
className: '',
children: null, children: null,
isContract: false, className: '',
hideName: false hideName: false,
isContract: false
}; };
render () { render () {
const { account, balance, className, children, hideName } = this.props; const { account, balance, children, className, hideName } = this.props;
const { address, meta, uuid } = account;
if (!account) { if (!account) {
return null; return null;
} }
const uuidText = !uuid const { address } = account;
? null const meta = account.meta || {};
: <div className={ styles.uuidline }>uuid: { uuid }</div>;
return ( return (
<div className={ className }> <div className={ className }>
<Container> <Container>
<IdentityIcon <IdentityIcon address={ address } />
address={ address } />
<div className={ styles.floatleft }> <div className={ styles.floatleft }>
{ this.renderName(address) } { this.renderName() }
<div className={ [ hideName ? styles.bigaddress : '', styles.addressline ].join(' ') }> <div className={ [ hideName ? styles.bigaddress : '', styles.addressline ].join(' ') }>
<CopyToClipboard data={ address } /> <CopyToClipboard data={ address } />
<div className={ styles.address }>{ address }</div> <div className={ styles.address }>{ address }</div>
</div> </div>
{ this.renderUuid() }
{ uuidText }
<div className={ styles.infoline }> <div className={ styles.infoline }>
{ meta.description } { meta.description }
</div> </div>
{ this.renderTxCount() } { this.renderTxCount() }
</div> </div>
<div className={ styles.tags }> <div className={ styles.tags }>
<Tags tags={ meta.tags } /> <Tags tags={ meta.tags } />
</div> </div>
@ -77,9 +73,7 @@ export default class Header extends Component {
<Balance <Balance
account={ account } account={ account }
balance={ balance } /> balance={ balance } />
<Certifications <Certifications address={ address } />
account={ account.address }
/>
</div> </div>
{ children } { children }
</Container> </Container>
@ -87,15 +81,22 @@ export default class Header extends Component {
); );
} }
renderName (address) { renderName () {
const { hideName } = this.props; const { hideName } = this.props;
if (hideName) { if (hideName) {
return null; return null;
} }
const { address } = this.props.account;
return ( return (
<ContainerTitle title={ <IdentityName address={ address } unknown /> } /> <ContainerTitle
title={
<IdentityName
address={ address }
unknown />
} />
); );
} }
@ -114,7 +115,31 @@ export default class Header extends Component {
return ( return (
<div className={ styles.infoline }> <div className={ styles.infoline }>
{ txCount.toFormat() } outgoing transactions <FormattedMessage
id='account.header.outgoingTransactions'
defaultMessage='{count} outgoing transactions'
values={ {
count: txCount.toFormat()
} } />
</div>
);
}
renderUuid () {
const { uuid } = this.props.account;
if (!uuid) {
return null;
}
return (
<div className={ styles.uuidline }>
<FormattedMessage
id='account.header.uuid'
defaultMessage='uuid: {uuid}'
values={ {
uuid
} } />
</div> </div>
); );
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
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(
<Header { ...props } />
);
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;
});
});
});

View File

@ -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 <http://www.gnu.org/licenses/>.
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
};
});
});
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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
}
]);
});
});
});
});
});

View File

@ -14,15 +14,18 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import etherscan from '~/3rdparty/etherscan';
import { Container, TxList, Loading } from '~/ui'; import { Container, TxList, Loading } from '~/ui';
import Store from './store';
import styles from './transactions.css'; import styles from './transactions.css';
@observer
class Transactions extends Component { class Transactions extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
@ -34,34 +37,35 @@ class Transactions extends Component {
traceMode: PropTypes.bool traceMode: PropTypes.bool
} }
state = { store = new Store(this.context.api);
hashes: [],
loading: true,
callInfo: {}
}
componentDidMount () { componentWillMount () {
this.getTransactions(this.props); this.store.updateProps(this.props);
} }
componentWillReceiveProps (newProps) { componentWillReceiveProps (newProps) {
if (this.props.traceMode === undefined && newProps.traceMode !== undefined) { if (this.props.traceMode === undefined && newProps.traceMode !== undefined) {
this.getTransactions(newProps); this.store.updateProps(newProps);
return; return;
} }
const hasChanged = [ 'isTest', 'address' ] const hasChanged = ['isTest', 'address']
.map(key => newProps[key] !== this.props[key]) .map(key => newProps[key] !== this.props[key])
.reduce((truth, keyTruth) => truth || keyTruth, false); .reduce((truth, keyTruth) => truth || keyTruth, false);
if (hasChanged) { if (hasChanged) {
this.getTransactions(newProps); this.store.updateProps(newProps);
} }
} }
render () { render () {
return ( return (
<Container title='transactions'> <Container
title={
<FormattedMessage
id='account.transactions.title'
defaultMessage='transactions' />
}>
{ this.renderTransactionList() } { this.renderTransactionList() }
{ this.renderEtherscanFooter() } { this.renderEtherscanFooter() }
</Container> </Container>
@ -69,10 +73,9 @@ class Transactions extends Component {
} }
renderTransactionList () { renderTransactionList () {
const { address } = this.props; const { address, isLoading, txHashes } = this.store;
const { hashes, loading } = this.state;
if (loading) { if (isLoading) {
return ( return (
<Loading /> <Loading />
); );
@ -81,85 +84,29 @@ class Transactions extends Component {
return ( return (
<TxList <TxList
address={ address } address={ address }
hashes={ hashes } hashes={ txHashes }
/> />
); );
} }
renderEtherscanFooter () { renderEtherscanFooter () {
const { traceMode } = this.props; const { isTracing } = this.store;
if (traceMode) { if (isTracing) {
return null; return null;
} }
return ( return (
<div className={ styles.etherscan }> <div className={ styles.etherscan }>
Transaction list powered by <a href='https://etherscan.io/' target='_blank'>etherscan.io</a> <FormattedMessage
id='account.transactions.poweredBy'
defaultMessage='Transaction list powered by {etherscan}'
values={ {
etherscan: <a href='https://etherscan.io/' target='_blank'>etherscan.io</a>
} } />
</div> </div>
); );
} }
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) { function mapStateToProps (state) {

View File

@ -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 <http://www.gnu.org/licenses/>.
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(
<Transactions
address={ ADDRESS }
{ ...props } />,
{ 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/);
});
});
});

View File

@ -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 <http://www.gnu.org/licenses/>.
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
};

View File

@ -14,47 +14,38 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from '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 shapeshiftBtn from '~/../assets/images/shapeshift-btn.png';
import { EditMeta, DeleteAccount, Shapeshift, Verification, Transfer, PasswordManager } from '~/modals';
import Header from './Header';
import Transactions from './Transactions';
import { setVisibleAccounts } from '~/redux/providers/personalActions'; import { setVisibleAccounts } from '~/redux/providers/personalActions';
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions'; 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'; import styles from './account.css';
@observer
class Account extends Component { class Account extends Component {
static propTypes = { static propTypes = {
setVisibleAccounts: PropTypes.func.isRequired,
fetchCertifiers: PropTypes.func.isRequired, fetchCertifiers: PropTypes.func.isRequired,
fetchCertifications: PropTypes.func.isRequired, fetchCertifications: PropTypes.func.isRequired,
images: PropTypes.object.isRequired, images: PropTypes.object.isRequired,
setVisibleAccounts: PropTypes.func.isRequired,
params: PropTypes.object,
accounts: PropTypes.object, accounts: PropTypes.object,
balances: PropTypes.object balances: PropTypes.object,
params: PropTypes.object
} }
state = { store = new Store();
showDeleteDialog: false,
showEditDialog: false,
showFundDialog: false,
showVerificationDialog: false,
showTransferDialog: false,
showPasswordDialog: false
}
componentDidMount () { componentDidMount () {
this.props.fetchCertifiers(); this.props.fetchCertifiers();
@ -76,7 +67,8 @@ class Account extends Component {
setVisibleAccounts (props = this.props) { setVisibleAccounts (props = this.props) {
const { params, setVisibleAccounts, fetchCertifications } = props; const { params, setVisibleAccounts, fetchCertifications } = props;
const addresses = [ params.address ]; const addresses = [params.address];
setVisibleAccounts(addresses); setVisibleAccounts(addresses);
fetchCertifications(params.address); fetchCertifications(params.address);
} }
@ -97,15 +89,14 @@ class Account extends Component {
{ this.renderDeleteDialog(account) } { this.renderDeleteDialog(account) }
{ this.renderEditDialog(account) } { this.renderEditDialog(account) }
{ this.renderFundDialog() } { this.renderFundDialog() }
{ this.renderPasswordDialog(account) }
{ this.renderTransferDialog(account, balance) }
{ this.renderVerificationDialog() } { this.renderVerificationDialog() }
{ this.renderTransferDialog() } { this.renderActionbar(balance) }
{ this.renderPasswordDialog() }
{ this.renderActionbar() }
<Page> <Page>
<Header <Header
account={ account } account={ account }
balance={ balance } balance={ balance } />
/>
<Transactions <Transactions
accounts={ accounts } accounts={ accounts }
address={ address } /> address={ address } />
@ -114,86 +105,108 @@ class Account extends Component {
); );
} }
renderActionbar () { renderActionbar (balance) {
const { address } = this.props.params;
const { balances } = this.props;
const balance = balances[address];
const showTransferButton = !!(balance && balance.tokens); const showTransferButton = !!(balance && balance.tokens);
const buttons = [ const buttons = [
<Button <Button
key='transferFunds'
icon={ <ContentSend /> }
label='transfer'
disabled={ !showTransferButton } disabled={ !showTransferButton }
onClick={ this.onTransferClick } />, icon={ <SendIcon /> }
key='transferFunds'
label={
<FormattedMessage
id='account.button.transfer'
defaultMessage='transfer' />
}
onClick={ this.store.toggleTransferDialog } />,
<Button <Button
icon={
<img
className={ styles.btnicon }
src={ shapeshiftBtn } />
}
key='shapeshift' key='shapeshift'
icon={ <img src={ shapeshiftBtn } className={ styles.btnicon } /> } label={
label='shapeshift' <FormattedMessage
onClick={ this.onShapeshiftAccountClick } />, id='account.button.shapeshift'
defaultMessage='shapeshift' />
}
onClick={ this.store.toggleFundDialog } />,
<Button <Button
key='sms-verification'
icon={ <VerifyIcon /> } icon={ <VerifyIcon /> }
label='Verify' key='sms-verification'
onClick={ this.openVerification } />, label={
<FormattedMessage
id='account.button.verify'
defaultMessage='verify' />
}
onClick={ this.store.toggleVerificationDialog } />,
<Button <Button
icon={ <EditIcon /> }
key='editmeta' key='editmeta'
icon={ <ContentCreate /> } label={
label='edit' <FormattedMessage
onClick={ this.onEditClick } />, id='account.button.edit'
defaultMessage='edit' />
}
onClick={ this.store.toggleEditDialog } />,
<Button <Button
icon={ <LockedIcon /> }
key='passwordManager' key='passwordManager'
icon={ <LockIcon /> } label={
label='password' <FormattedMessage
onClick={ this.onPasswordClick } />, id='account.button.password'
defaultMessage='password' />
}
onClick={ this.store.togglePasswordDialog } />,
<Button <Button
icon={ <DeleteIcon /> }
key='delete' key='delete'
icon={ <ActionDelete /> } label={
label='delete account' <FormattedMessage
onClick={ this.onDeleteClick } /> id='account.button.delete'
defaultMessage='delete account' />
}
onClick={ this.store.toggleDeleteDialog } />
]; ];
return ( return (
<Actionbar <Actionbar
title='Account Management' buttons={ buttons }
buttons={ buttons } /> title={
<FormattedMessage
id='account.title'
defaultMessage='Account Management' />
} />
); );
} }
renderDeleteDialog (account) { renderDeleteDialog (account) {
const { showDeleteDialog } = this.state; if (!this.store.isDeleteVisible) {
if (!showDeleteDialog) {
return null; return null;
} }
return ( return (
<DeleteAccount <DeleteAccount
account={ account } account={ account }
onClose={ this.onDeleteClose } /> onClose={ this.store.toggleDeleteDialog } />
); );
} }
renderEditDialog (account) { renderEditDialog (account) {
const { showEditDialog } = this.state; if (!this.store.isEditVisible) {
if (!showEditDialog) {
return null; return null;
} }
return ( return (
<EditMeta <EditMeta
account={ account } account={ account }
onClose={ this.onEditClick } /> onClose={ this.store.toggleEditDialog } />
); );
} }
renderFundDialog () { renderFundDialog () {
const { showFundDialog } = this.state; if (!this.store.isFundVisible) {
if (!showFundDialog) {
return null; return null;
} }
@ -202,12 +215,41 @@ class Account extends Component {
return ( return (
<Shapeshift <Shapeshift
address={ address } address={ address }
onClose={ this.onShapeshiftAccountClose } /> onClose={ this.store.toggleFundDialog } />
);
}
renderPasswordDialog (account) {
if (!this.store.isPasswordVisible) {
return null;
}
return (
<PasswordManager
account={ account }
onClose={ this.store.togglePasswordDialog } />
);
}
renderTransferDialog (account, balance) {
if (!this.store.isTransferVisible) {
return null;
}
const { balances, images } = this.props;
return (
<Transfer
account={ account }
balance={ balance }
balances={ balances }
images={ images }
onClose={ this.store.toggleTransferDialog } />
); );
} }
renderVerificationDialog () { renderVerificationDialog () {
if (!this.state.showVerificationDialog) { if (!this.store.isVerificationVisible) {
return null; return null;
} }
@ -216,102 +258,9 @@ class Account extends Component {
return ( return (
<Verification <Verification
account={ address } account={ address }
onClose={ this.onVerificationClose } onClose={ this.store.toggleVerificationDialog } />
/>
); );
} }
renderTransferDialog () {
const { showTransferDialog } = this.state;
if (!showTransferDialog) {
return null;
}
const { address } = this.props.params;
const { accounts, balances, images } = this.props;
const account = accounts[address];
const balance = balances[address];
return (
<Transfer
account={ account }
balance={ balance }
balances={ balances }
images={ images }
onClose={ this.onTransferClose } />
);
}
renderPasswordDialog () {
const { showPasswordDialog } = this.state;
if (!showPasswordDialog) {
return null;
}
const { address } = this.props.params;
const { accounts } = this.props;
const account = accounts[address];
return (
<PasswordManager
account={ account }
onClose={ this.onPasswordClose } />
);
}
onDeleteClick = () => {
this.setState({ showDeleteDialog: true });
}
onDeleteClose = () => {
this.setState({ showDeleteDialog: false });
}
onEditClick = () => {
this.setState({
showEditDialog: !this.state.showEditDialog
});
}
onShapeshiftAccountClick = () => {
this.setState({
showFundDialog: !this.state.showFundDialog
});
}
onShapeshiftAccountClose = () => {
this.onShapeshiftAccountClick();
}
openVerification = () => {
this.setState({ showVerificationDialog: true });
}
onVerificationClose = () => {
this.setState({ showVerificationDialog: false });
}
onTransferClick = () => {
this.setState({
showTransferDialog: !this.state.showTransferDialog
});
}
onTransferClose = () => {
this.onTransferClick();
}
onPasswordClick = () => {
this.setState({
showPasswordDialog: !this.state.showPasswordDialog
});
}
onPasswordClose = () => {
this.onPasswordClick();
}
} }
function mapStateToProps (state) { function mapStateToProps (state) {
@ -328,9 +277,9 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {
return bindActionCreators({ return bindActionCreators({
setVisibleAccounts,
fetchCertifiers, fetchCertifiers,
fetchCertifications fetchCertifications,
setVisibleAccounts
}, dispatch); }, dispatch);
} }

View File

@ -0,0 +1,226 @@
// 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 <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import { ADDRESS, createRedux } from './account.test.js';
import Account from './';
let component;
let instance;
let store;
function render (props) {
component = shallow(
<Account
params={ { address: ADDRESS } }
{ ...props } />,
{ context: { store: createRedux() } }
).find('Account').shallow();
instance = component.instance();
store = instance.store;
return component;
}
describe('views/Account', () => {
describe('rendering', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('sections', () => {
it('renders the Actionbar', () => {
expect(component.find('Actionbar')).to.have.length(1);
});
it('renders the Page', () => {
expect(component.find('Page')).to.have.length(1);
});
it('renders the Header', () => {
expect(component.find('Header')).to.have.length(1);
});
it('renders the Transactions', () => {
expect(component.find('Connect(Transactions)')).to.have.length(1);
});
it('renders no other sections', () => {
expect(component.find('div').children()).to.have.length(2);
});
});
});
describe('sub-renderers', () => {
describe('renderActionBar', () => {
let bar;
let barShallow;
beforeEach(() => {
render();
bar = instance.renderActionbar({ tokens: {} });
barShallow = shallow(bar);
});
it('renders the bar', () => {
expect(bar.type).to.match(/Actionbar/);
});
// TODO: Finding by index is not optimal, however couldn't find a better method atm
// since we cannot find by key (prop not visible in shallow debug())
describe('clicks', () => {
it('toggles transfer on click', () => {
barShallow.find('Button').at(0).simulate('click');
expect(store.isTransferVisible).to.be.true;
});
it('toggles fund on click', () => {
barShallow.find('Button').at(1).simulate('click');
expect(store.isFundVisible).to.be.true;
});
it('toggles fund on click', () => {
barShallow.find('Button').at(1).simulate('click');
expect(store.isFundVisible).to.be.true;
});
it('toggles verify on click', () => {
barShallow.find('Button').at(2).simulate('click');
expect(store.isVerificationVisible).to.be.true;
});
it('toggles edit on click', () => {
barShallow.find('Button').at(3).simulate('click');
expect(store.isEditVisible).to.be.true;
});
it('toggles password on click', () => {
barShallow.find('Button').at(4).simulate('click');
expect(store.isPasswordVisible).to.be.true;
});
it('toggles delete on click', () => {
barShallow.find('Button').at(5).simulate('click');
expect(store.isDeleteVisible).to.be.true;
});
});
});
describe('renderDeleteDialog', () => {
it('renders null when not visible', () => {
render();
expect(store.isDeleteVisible).to.be.false;
expect(instance.renderDeleteDialog()).to.be.null;
});
it('renders the modal when visible', () => {
render();
store.toggleDeleteDialog();
expect(instance.renderDeleteDialog().type).to.match(/Connect/);
});
});
describe('renderEditDialog', () => {
it('renders null when not visible', () => {
render();
expect(store.isEditVisible).to.be.false;
expect(instance.renderEditDialog()).to.be.null;
});
it('renders the modal when visible', () => {
render();
store.toggleEditDialog();
expect(instance.renderEditDialog({ address: ADDRESS }).type).to.match(/Connect/);
});
});
describe('renderFundDialog', () => {
it('renders null when not visible', () => {
render();
expect(store.isFundVisible).to.be.false;
expect(instance.renderFundDialog()).to.be.null;
});
it('renders the modal when visible', () => {
render();
store.toggleFundDialog();
expect(instance.renderFundDialog().type).to.match(/Shapeshift/);
});
});
describe('renderPasswordDialog', () => {
it('renders null when not visible', () => {
render();
expect(store.isPasswordVisible).to.be.false;
expect(instance.renderPasswordDialog()).to.be.null;
});
it('renders the modal when visible', () => {
render();
store.togglePasswordDialog();
expect(instance.renderPasswordDialog({ address: ADDRESS }).type).to.match(/Connect/);
});
});
describe('renderTransferDialog', () => {
it('renders null when not visible', () => {
render();
expect(store.isTransferVisible).to.be.false;
expect(instance.renderTransferDialog()).to.be.null;
});
it('renders the modal when visible', () => {
render();
store.toggleTransferDialog();
expect(instance.renderTransferDialog().type).to.match(/Connect/);
});
});
describe('renderVerificationDialog', () => {
it('renders null when not visible', () => {
render();
expect(store.isVerificationVisible).to.be.false;
expect(instance.renderVerificationDialog()).to.be.null;
});
it('renders the modal when visible', () => {
render();
store.toggleVerificationDialog();
expect(instance.renderVerificationDialog().type).to.match(/Connect/);
});
});
});
});

View File

@ -0,0 +1,52 @@
// 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 <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
const ADDRESS = '0x0123456789012345678901234567890123456789';
function createRedux () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
balances: {
balances: {
[ADDRESS]: {}
}
},
images: {},
nodeStatus: {
isTest: false,
traceMode: false
},
personal: {
accounts: {
[ADDRESS]: {
address: ADDRESS
}
}
}
};
}
};
}
export {
ADDRESS,
createRedux
};

View File

@ -0,0 +1,50 @@
// 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 <http://www.gnu.org/licenses/>.
import { action, observable } from 'mobx';
export default class Store {
@observable isDeleteVisible = false;
@observable isEditVisible = false;
@observable isFundVisible = false;
@observable isPasswordVisible = false;
@observable isTransferVisible = false;
@observable isVerificationVisible = false;
@action toggleDeleteDialog = () => {
this.isDeleteVisible = !this.isDeleteVisible;
}
@action toggleEditDialog = () => {
this.isEditVisible = !this.isEditVisible;
}
@action toggleFundDialog = () => {
this.isFundVisible = !this.isFundVisible;
}
@action togglePasswordDialog = () => {
this.isPasswordVisible = !this.isPasswordVisible;
}
@action toggleTransferDialog = () => {
this.isTransferVisible = !this.isTransferVisible;
}
@action toggleVerificationDialog = () => {
this.isVerificationVisible = !this.isVerificationVisible;
}
}

View File

@ -0,0 +1,84 @@
// 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 <http://www.gnu.org/licenses/>.
import Store from './store';
let store;
function createStore () {
store = new Store();
}
describe('views/Account/Store', () => {
beforeEach(() => {
createStore();
});
describe('constructor', () => {
it('sets all modal visibility to false', () => {
expect(store.isDeleteVisible).to.be.false;
expect(store.isEditVisible).to.be.false;
expect(store.isFundVisible).to.be.false;
expect(store.isPasswordVisible).to.be.false;
expect(store.isTransferVisible).to.be.false;
expect(store.isVerificationVisible).to.be.false;
});
});
describe('@action', () => {
describe('toggleDeleteDialog', () => {
it('toggles the visibility', () => {
store.toggleDeleteDialog();
expect(store.isDeleteVisible).to.be.true;
});
});
describe('toggleEditDialog', () => {
it('toggles the visibility', () => {
store.toggleEditDialog();
expect(store.isEditVisible).to.be.true;
});
});
describe('toggleFundDialog', () => {
it('toggles the visibility', () => {
store.toggleFundDialog();
expect(store.isFundVisible).to.be.true;
});
});
describe('togglePasswordDialog', () => {
it('toggles the visibility', () => {
store.togglePasswordDialog();
expect(store.isPasswordVisible).to.be.true;
});
});
describe('toggleTransferDialog', () => {
it('toggles the visibility', () => {
store.toggleTransferDialog();
expect(store.isTransferVisible).to.be.true;
});
});
describe('toggleVerificationDialog', () => {
it('toggles the visibility', () => {
store.toggleVerificationDialog();
expect(store.isVerificationVisible).to.be.true;
});
});
});
});

View File

@ -197,7 +197,7 @@ export default class Summary extends Component {
} }
return ( return (
<Certifications account={ account.address } /> <Certifications address={ account.address } />
); );
} }
} }

View File

@ -17,13 +17,9 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; 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 { Input } from '~/ui';
import { CompareIcon, ComputerIcon, DashboardIcon, VpnIcon } from '~/ui/Icons';
import styles from './connection.css'; import styles from './connection.css';
@ -51,13 +47,6 @@ class Connection extends Component {
return null; return null;
} }
const typeIcon = needsToken
? <NotificationVpnLock className={ styles.svg } />
: <ActionDashboard className={ styles.svg } />;
const description = needsToken
? this.renderSigner()
: this.renderPing();
return ( return (
<div> <div>
<div className={ styles.overlay } /> <div className={ styles.overlay } />
@ -65,16 +54,24 @@ class Connection extends Component {
<div className={ styles.body }> <div className={ styles.body }>
<div className={ styles.icons }> <div className={ styles.icons }>
<div className={ styles.icon }> <div className={ styles.icon }>
<HardwareDesktopMac className={ styles.svg } /> <ComputerIcon className={ styles.svg } />
</div> </div>
<div className={ styles.iconSmall }> <div className={ styles.iconSmall }>
<ActionCompareArrows className={ `${styles.svg} ${styles.pulse}` } /> <CompareIcon className={ `${styles.svg} ${styles.pulse}` } />
</div> </div>
<div className={ styles.icon }> <div className={ styles.icon }>
{ typeIcon } {
needsToken
? <VpnIcon className={ styles.svg } />
: <DashboardIcon className={ styles.svg } />
}
</div> </div>
</div> </div>
{ description } {
needsToken
? this.renderSigner()
: this.renderPing()
}
</div> </div>
</div> </div>
</div> </div>
@ -144,10 +141,19 @@ class Connection extends Component {
); );
} }
onChangeToken = (event, _token) => { validateToken = (_token) => {
const token = _token.trim(); 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); 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 }, () => { this.setState({ token, validToken }, () => {
validToken && this.setToken(); validToken && this.setToken();
}); });
@ -159,7 +165,7 @@ class Connection extends Component {
this.setState({ loading: true }); this.setState({ loading: true });
api return api
.updateToken(token, 0) .updateToken(token, 0)
.then((isValid) => { .then((isValid) => {
this.setState({ this.setState({
@ -173,14 +179,14 @@ class Connection extends Component {
function mapStateToProps (state) { function mapStateToProps (state) {
const { isConnected, isConnecting, needsToken } = state.nodeStatus; const { isConnected, isConnecting, needsToken } = state.nodeStatus;
return { isConnected, isConnecting, needsToken }; return {
} isConnected,
isConnecting,
function mapDispatchToProps (dispatch) { needsToken
return bindActionCreators({}, dispatch); };
} }
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps null
)(Connection); )(Connection);

View File

@ -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 <http://www.gnu.org/licenses/>.
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(
<Connection />,
{ 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');
});
});
});
});

View File

@ -20,3 +20,23 @@
height: 100%; height: 100%;
width: 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;
}
}

View File

@ -16,6 +16,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { FormattedMessage } from 'react-intl';
import DappsStore from '../Dapps/dappsStore'; import DappsStore from '../Dapps/dappsStore';
@ -25,21 +26,71 @@ import styles from './dapp.css';
export default class Dapp extends Component { export default class Dapp extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
} };
static propTypes = { static propTypes = {
params: PropTypes.object params: PropTypes.object
}; };
state = {
app: null,
loading: true
};
store = DappsStore.get(this.context.api); 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 () { render () {
const { dappsUrl } = this.context.api; const { dappsUrl } = this.context.api;
const { id } = this.props.params; const { app, loading } = this.state;
const app = this.store.apps.find((app) => app.id === id);
if (loading) {
return (
<div className={ styles.full }>
<div className={ styles.text }>
<FormattedMessage
id='dapp.loading'
defaultMessage='Loading'
/>
</div>
</div>
);
}
if (!app) { if (!app) {
return null; return (
<div className={ styles.full }>
<div className={ styles.text }>
<FormattedMessage
id='dapp.unavailable'
defaultMessage='The dapp cannot be reached'
/>
</div>
</div>
);
} }
let src = null; let src = null;

View File

@ -45,6 +45,10 @@ class Dapps extends Component {
store = DappsStore.get(this.context.api); store = DappsStore.get(this.context.api);
permissionStore = new PermissionStore(this.context.api); permissionStore = new PermissionStore(this.context.api);
componentWillMount () {
this.store.loadAllApps();
}
render () { render () {
let externalOverlay = null; let externalOverlay = null;
if (this.store.externalOverlayVisible) { if (this.store.externalOverlayVisible) {

View File

@ -48,17 +48,43 @@ export default class DappsStore {
this.readDisplayApps(); this.readDisplayApps();
this.loadExternalOverlay(); this.loadExternalOverlay();
this.loadApps();
this.subscribeToChanges(); 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(); 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([ .all([
this.fetchBuiltinApps().then((apps) => this.addApps(apps)), 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)) this.fetchRegistryApps(dappReg).then((apps) => this.addApps(apps))
]) ])
.then(this.writeDisplayApps); .then(this.writeDisplayApps);
@ -67,8 +93,6 @@ export default class DappsStore {
static get (api) { static get (api) {
if (!instance) { if (!instance) {
instance = new DappsStore(api); instance = new DappsStore(api);
} else {
instance.loadApps();
} }
return instance; return instance;

View File

@ -20,6 +20,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import RaisedButton from 'material-ui/RaisedButton'; import RaisedButton from 'material-ui/RaisedButton';
import ReactTooltip from 'react-tooltip'; import ReactTooltip from 'react-tooltip';
import keycode from 'keycode';
import { Form, Input, IdentityIcon } from '~/ui'; import { Form, Input, IdentityIcon } from '~/ui';
@ -207,7 +208,9 @@ class TransactionPendingFormConfirm extends Component {
} }
onKeyDown = (event) => { onKeyDown = (event) => {
if (event.which !== 13) { const codeName = keycode(event);
if (codeName !== 'enter') {
return; return;
} }

View File

@ -71,7 +71,7 @@ class Wallet extends Component {
owned: PropTypes.bool.isRequired, owned: PropTypes.bool.isRequired,
setVisibleAccounts: PropTypes.func.isRequired, setVisibleAccounts: PropTypes.func.isRequired,
wallet: PropTypes.object.isRequired, wallet: PropTypes.object.isRequired,
walletAccount: nullableProptype(PropTypes.object).isRequired walletAccount: nullableProptype(PropTypes.object.isRequired)
}; };
state = { state = {
@ -180,7 +180,7 @@ class Wallet extends Component {
const { address, isTest, wallet } = this.props; const { address, isTest, wallet } = this.props;
const { owners, require, confirmations, transactions } = wallet; const { owners, require, confirmations, transactions } = wallet;
if (!isTest || !owners || !require) { if (!owners || !require) {
return ( return (
<div style={ { marginTop: '4em' } }> <div style={ { marginTop: '4em' } }>
<Loading size={ 4 } /> <Loading size={ 4 } />

View File

@ -26,6 +26,7 @@ injectTapEventPlugin();
import chai from 'chai'; import chai from 'chai';
import chaiAsPromised from 'chai-as-promised'; import chaiAsPromised from 'chai-as-promised';
import chaiEnzyme from 'chai-enzyme'; import chaiEnzyme from 'chai-enzyme';
import 'sinon-as-promised';
import sinonChai from 'sinon-chai'; import sinonChai from 'sinon-chai';
import { WebSocket } from 'mock-socket'; import { WebSocket } from 'mock-socket';
import jsdom from 'jsdom'; import jsdom from 'jsdom';

View File

@ -59,9 +59,9 @@ impl BlockChain {
mix_hash: self.genesis_block.mix_hash.clone(), mix_hash: self.genesis_block.mix_hash.clone(),
}), }),
difficulty: self.genesis_block.difficulty, difficulty: self.genesis_block.difficulty,
author: self.genesis_block.author.clone(), author: Some(self.genesis_block.author.clone()),
timestamp: self.genesis_block.timestamp, timestamp: Some(self.genesis_block.timestamp),
parent_hash: self.genesis_block.parent_hash.clone(), parent_hash: Some(self.genesis_block.parent_hash.clone()),
gas_limit: self.genesis_block.gas_limit, gas_limit: self.genesis_block.gas_limit,
transactions_root: Some(self.genesis_block.transactions_root.clone()), transactions_root: Some(self.genesis_block.transactions_root.clone()),
receipts_root: Some(self.genesis_block.receipts_root.clone()), receipts_root: Some(self.genesis_block.receipts_root.clone()),

View File

@ -28,13 +28,13 @@ pub struct Genesis {
pub seal: Seal, pub seal: Seal,
/// Difficulty. /// Difficulty.
pub difficulty: Uint, pub difficulty: Uint,
/// Block author. /// Block author, defaults to 0.
pub author: Address, pub author: Option<Address>,
/// Block timestamp. /// Block timestamp, defaults to 0.
pub timestamp: Uint, pub timestamp: Option<Uint>,
/// Parent hash. /// Parent hash, defaults to 0.
#[serde(rename="parentHash")] #[serde(rename="parentHash")]
pub parent_hash: H256, pub parent_hash: Option<H256>,
/// Gas limit. /// Gas limit.
#[serde(rename="gasLimit")] #[serde(rename="gasLimit")]
pub gas_limit: Uint, pub gas_limit: Uint,

View File

@ -22,9 +22,9 @@ use hash::H256;
/// Spec params. /// Spec params.
#[derive(Debug, PartialEq, Deserialize)] #[derive(Debug, PartialEq, Deserialize)]
pub struct Params { pub struct Params {
/// Account start nonce. /// Account start nonce, defaults to 0.
#[serde(rename="accountStartNonce")] #[serde(rename="accountStartNonce")]
pub account_start_nonce: Uint, pub account_start_nonce: Option<Uint>,
/// Maximum size of extra data. /// Maximum size of extra data.
#[serde(rename="maximumExtraDataSize")] #[serde(rename="maximumExtraDataSize")]
pub maximum_extra_data_size: Uint, pub maximum_extra_data_size: Uint,

Some files were not shown because too many files have changed in this diff Show More