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]]
name = "parity-ui-precompiled"
version = "1.4.0"
source = "git+https://github.com/ethcore/js-precompiled.git#ebea2bf78e076916b51b04d8b24187a6a85ae440"
source = "git+https://github.com/ethcore/js-precompiled.git#257b3ce8aaa6797507592200dc78b29b8a305c3f"
dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

View File

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

View File

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

View File

@ -40,7 +40,6 @@ use util::sha3::SHA3_NULL_RLP;
use rlp::{RlpStream, Stream, UntrustedRlp, View};
use bloom_journal::Bloom;
use self::account::Account;
use self::block::AbridgedBlock;
use self::io::SnapshotWriter;
@ -368,12 +367,12 @@ pub fn chunk_state<'a>(db: &HashDB, root: &H256, writer: &Mutex<SnapshotWriter +
// account_key here is the address' hash.
for item in account_trie.iter()? {
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_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)?;
}
@ -507,10 +506,10 @@ fn rebuild_accounts(
// fill out the storage trie and code while decoding.
let (acc, maybe_code) = {
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 {
// new inline code
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);

View File

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

View File

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

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// 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 ethjson;
use super::seal::Seal;
@ -50,9 +50,9 @@ impl From<ethjson::spec::Genesis> for Genesis {
Genesis {
seal: From::from(g.seal),
difficulty: g.difficulty.into(),
author: g.author.into(),
timestamp: g.timestamp.into(),
parent_hash: g.parent_hash.into(),
author: g.author.map_or_else(Address::zero, Into::into),
timestamp: g.timestamp.map_or(0, Into::into),
parent_hash: g.parent_hash.map_or_else(H256::zero, Into::into),
gas_limit: g.gas_limit.into(),
transactions_root: g.transactions_root.map_or_else(|| SHA3_NULL_RLP.clone(), Into::into),
receipts_root: g.receipts_root.map_or_else(|| SHA3_NULL_RLP.clone(), Into::into),

View File

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

View File

@ -20,6 +20,7 @@ use util::*;
use pod_account::*;
use rlp::*;
use lru_cache::LruCache;
use basic_account::BasicAccount;
use std::cell::{RefCell, Cell};
@ -53,6 +54,23 @@ pub struct Account {
address_hash: Cell<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 {
#[cfg(test)]
/// General constructor.
@ -109,19 +127,8 @@ impl Account {
/// Create a new account from RLP.
pub fn from_rlp(rlp: &[u8]) -> Account {
let r: Rlp = Rlp::new(rlp);
Account {
nonce: r.val_at(0),
balance: r.val_at(1),
storage_root: r.val_at(2),
storage_cache: Self::empty_storage_cache(),
storage_changes: HashMap::new(),
code_hash: r.val_at(3),
code_cache: Arc::new(vec![]),
code_size: None,
code_filth: Filth::Clean,
address_hash: Cell::new(None),
}
let basic: BasicAccount = ::rlp::decode(rlp);
basic.into()
}
/// Create a new contract account.

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 security_level;
pub mod encoded;
pub mod basic_account;

View File

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

View File

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

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

View File

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

View File

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

View File

@ -14,13 +14,29 @@
// You should have received a copy of the GNU General Public License
// 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 mockget = helpers.mockget;
const mockpost = helpers.mockpost;
const shapeshift = helpers.shapeshift;
describe('shapeshift/calls', () => {
let clock;
let shapeshift;
beforeEach(() => {
clock = sinon.useFakeTimers();
shapeshift = initShapeshift(helpers.APIKEY);
});
afterEach(() => {
clock.restore();
});
describe('getCoins', () => {
const REPLY = {
BTC: {
@ -39,8 +55,8 @@ describe('shapeshift/calls', () => {
let scope;
before(() => {
scope = mockget([{ path: 'getcoins', reply: REPLY }]);
beforeEach(() => {
scope = mockget(shapeshift, [{ path: 'getcoins', reply: REPLY }]);
return shapeshift.getCoins();
});
@ -61,8 +77,8 @@ describe('shapeshift/calls', () => {
let scope;
before(() => {
scope = mockget([{ path: 'marketinfo/btc_ltc', reply: REPLY }]);
beforeEach(() => {
scope = mockget(shapeshift, [{ path: 'marketinfo/btc_ltc', reply: REPLY }]);
return shapeshift.getMarketInfo('btc_ltc');
});
@ -80,8 +96,8 @@ describe('shapeshift/calls', () => {
let scope;
before(() => {
scope = mockget([{ path: 'txStat/0x123', reply: REPLY }]);
beforeEach(() => {
scope = mockget(shapeshift, [{ path: 'txStat/0x123', reply: REPLY }]);
return shapeshift.getStatus('0x123');
});
@ -101,8 +117,8 @@ describe('shapeshift/calls', () => {
let scope;
before(() => {
scope = mockpost([{ path: 'shift', reply: REPLY }]);
beforeEach(() => {
scope = mockpost(shapeshift, [{ path: 'shift', reply: REPLY }]);
return shapeshift.shift('0x456', '1BTC', 'btc_eth');
});
@ -125,4 +141,80 @@ describe('shapeshift/calls', () => {
});
});
});
describe('subscriptions', () => {
const ADDRESS = '0123456789abcdef';
const REPLY = {
status: 'complete',
address: ADDRESS
};
let callback;
beforeEach(() => {
mockget(shapeshift, [{ path: `txStat/${ADDRESS}`, reply: REPLY }]);
callback = sinon.stub();
shapeshift.subscribe(ADDRESS, callback);
});
describe('subscribe', () => {
it('adds the depositAddress to the list', () => {
const subscriptions = shapeshift._getSubscriptions();
expect(subscriptions.length).to.equal(1);
expect(subscriptions[0].depositAddress).to.equal(ADDRESS);
});
it('starts the polling timer', () => {
expect(shapeshift._isPolling()).to.be.true;
});
it('calls the callback once the timer has elapsed', () => {
clock.tick(2222);
return shapeshift._getSubscriptionPromises().then(() => {
expect(callback).to.have.been.calledWith(null, REPLY);
});
});
it('auto-unsubscribes on completed', () => {
clock.tick(2222);
return shapeshift._getSubscriptionPromises().then(() => {
expect(shapeshift._getSubscriptions().length).to.equal(0);
});
});
});
describe('unsubscribe', () => {
it('unbsubscribes when requested', () => {
expect(shapeshift._getSubscriptions().length).to.equal(1);
shapeshift.unsubscribe(ADDRESS);
expect(shapeshift._getSubscriptions().length).to.equal(0);
});
it('clears the polling on no subscriptions', () => {
shapeshift.unsubscribe(ADDRESS);
expect(shapeshift._isPolling()).to.be.false;
});
it('handles unsubscribe of auto-unsubscribe', () => {
clock.tick(2222);
return shapeshift._getSubscriptionPromises().then(() => {
expect(shapeshift.unsubscribe(ADDRESS)).to.be.true;
});
});
it('handles unsubscribe when multiples listed', () => {
const ADDRESS2 = 'abcdef0123456789';
shapeshift.subscribe(ADDRESS2, sinon.stub());
expect(shapeshift._getSubscriptions().length).to.equal(2);
expect(shapeshift._getSubscriptions()[0].depositAddress).to.equal(ADDRESS);
shapeshift.unsubscribe(ADDRESS);
expect(shapeshift._getSubscriptions()[0].depositAddress).to.equal(ADDRESS2);
});
});
});
});

View File

@ -31,6 +31,12 @@ export default class Param {
}
static toParams (params) {
return params.map((param) => new Param(param.name, param.type));
return params.map((param) => {
if (param instanceof Param) {
return param;
}
return new Param(param.name, param.type);
});
}
}

View File

@ -34,5 +34,14 @@ describe('abi/spec/Param', () => {
expect(params[0].name).to.equal('foo');
expect(params[0].kind.type).to.equal('uint');
});
it('converts only if needed', () => {
const _params = Param.toParams([{ name: 'foo', type: 'uint' }]);
const params = Param.toParams(_params);
expect(params.length).to.equal(1);
expect(params[0].name).to.equal('foo');
expect(params[0].kind.type).to.equal('uint');
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

@ -14,8 +14,21 @@
// You should have received a copy of the GNU General Public License
// 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) {
return `0x${keccak_256(value)}`;
export function sha3 (value, options) {
if (options && options.encoding === 'hex') {
if (value.length > 2 && value.substr(0, 2) === '0x') {
value = value.substr(2);
}
value = CryptoJS.enc.Hex.parse(value);
}
const hash = CryptoSha3(value, {
outputLength: 256
}).toString();
return `0x${hash}`;
}

View File

@ -34,3 +34,16 @@ ReactDOM.render(
</Provider>,
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 = {
accounts: PropTypes.object.isRequired,
contract: nullableProptype(PropTypes.object).isRequired,
fee: nullableProptype(PropTypes.object).isRequired
contract: nullableProptype(PropTypes.object.isRequired),
fee: nullableProptype(PropTypes.object.isRequired)
};
render () {

View File

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

View File

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

View File

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

View File

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

View File

@ -35,7 +35,12 @@
.link {
color: #00BCD4;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.link:hover {
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 CheckIcon from 'material-ui/svg-icons/navigation/check';
import { nullableProptype } from '~/util/proptypes';
import { fromWei } from '../parity.js';
import { reserve, drop } from './actions';
import { clearError, reserve, drop } from './actions';
import styles from './names.css';
const useSignerText = (<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 {
static propTypes = {
error: nullableProptype(PropTypes.object.isRequired),
fee: PropTypes.object.isRequired,
pending: PropTypes.bool.isRequired,
queue: PropTypes.array.isRequired,
clearError: PropTypes.func.isRequired,
reserve: PropTypes.func.isRequired,
drop: PropTypes.func.isRequired
}
};
state = {
action: 'reserve',
name: ''
};
componentWillReceiveProps (nextProps) {
const nextQueue = nextProps.queue;
const prevQueue = this.props.queue;
if (nextQueue.length > prevQueue.length) {
const newQueued = nextQueue[nextQueue.length - 1];
const newName = newQueued.name;
if (newName !== this.state.name) {
return;
}
this.setState({ name: '' });
}
}
render () {
const { action, name } = this.state;
const { fee, pending, queue } = this.props;
@ -122,6 +109,7 @@ class Names extends Component {
: (<p className={ styles.noSpacing }>To drop a name, you have to be the owner.</p>)
)
}
{ this.renderError() }
<div className={ styles.box }>
<TextField
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) => {
this.clearError();
this.setState({ name: e.target.value });
};
onActionChange = (e, i, action) => {
this.clearError();
this.setState({ action });
};
onSubmitClick = () => {
const { action, name } = this.state;
if (action === 'reserve') {
this.props.reserve(name);
} else if (action === 'drop') {
this.props.drop(name);
return this.props.reserve(name);
}
if (action === 'drop') {
return this.props.drop(name);
}
};
clearError = () => {
if (this.props.error) {
this.props.clearError();
}
};
}
const mapStateToProps = (state) => ({ ...state.names, fee: state.fee });
const mapDispatchToProps = (dispatch) => bindActionCreators({ reserve, drop }, dispatch);
const mapDispatchToProps = (dispatch) => bindActionCreators({ clearError, reserve, drop }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Names);

View File

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

View File

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

View File

@ -36,3 +36,7 @@
flex-grow: 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 SaveIcon from 'material-ui/svg-icons/content/save';
import { update } from './actions';
import { nullableProptype } from '~/util/proptypes';
import { clearError, update } from './actions';
import styles from './records.css';
class Records extends Component {
static propTypes = {
error: nullableProptype(PropTypes.object.isRequired),
pending: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
clearError: PropTypes.func.isRequired,
update: PropTypes.func.isRequired
}
@ -53,6 +56,7 @@ class Records extends Component {
<p className={ styles.noSpacing }>
You can only modify entries of names that you previously registered.
</p>
{ this.renderError() }
<div className={ styles.box }>
<TextField
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) => {
this.clearError();
this.setState({ name: e.target.value });
};
onTypeChange = (e, i, type) => {
this.setState({ type });
};
onValueChange = (e) => {
this.setState({ value: e.target.value });
};
onSaveClick = () => {
const { name, type, value } = this.state;
this.props.update(name, type, value);
};
clearError = () => {
if (this.props.error) {
this.props.clearError();
}
};
}
const mapStateToProps = (state) => state.records;
const mapDispatchToProps = (dispatch) => bindActionCreators({ update }, dispatch);
const mapDispatchToProps = (dispatch) => bindActionCreators({ clearError, update }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Records);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ export const isAction = (ns, type, action) => {
};
export const isStage = (stage, action) => {
return action.type.slice(-1 - stage.length) === ` ${stage}`;
return (new RegExp(`${stage}$`)).test(action.type);
};
export const addToQueue = (queue, action, name) => {
@ -27,5 +27,5 @@ export const addToQueue = (queue, action, name) => {
};
export const removeFromQueue = (queue, action, name) => {
return queue.filter((e) => e.action === action && e.name === name);
return queue.filter((e) => !(e.action === action && e.name === name));
};

View File

@ -24,12 +24,6 @@ const postTx = (api, method, opt = {}, values = []) => {
})
.then((reqId) => {
return api.pollMethod('parity_checkRequest', reqId);
})
.catch((err) => {
if (err && err.type === 'REQUEST_REJECTED') {
throw new Error('The request has been rejected.');
}
throw err;
});
};

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/>.
import { showSnackbar } from './snackbarActions';
import { DEFAULT_NETCHAIN } from './statusReducer';
export default class ChainMiddleware {
toMiddleware () {
@ -23,11 +24,11 @@ export default class ChainMiddleware {
const { collection } = action;
if (collection && collection.netChain) {
const chain = collection.netChain;
const newChain = collection.netChain;
const { nodeStatus } = store.getState();
if (chain !== nodeStatus.netChain) {
store.dispatch(showSnackbar(`Switched to ${chain}. Please reload the page.`, 5000));
if (newChain !== nodeStatus.netChain && nodeStatus.netChain !== DEFAULT_NETCHAIN) {
store.dispatch(showSnackbar(`Switched to ${newChain}. Please reload the page.`, 60000));
}
}
}

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

View File

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

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
@ -39,7 +40,12 @@ class BlockStatus extends Component {
if (!syncing) {
return (
<div className={ styles.blockNumber }>
{ blockNumber.toFormat() } best block
<FormattedMessage
id='ui.blockStatus.bestBlock'
defaultMessage='{blockNumber} best block'
values={ {
blockNumber: blockNumber.toFormat()
} } />
</div>
);
}
@ -47,26 +53,45 @@ class BlockStatus extends Component {
if (syncing.warpChunksAmount && syncing.warpChunksProcessed && !syncing.warpChunksAmount.eq(syncing.warpChunksProcessed)) {
return (
<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>
);
}
let syncStatus = 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) {
const [first, last] = syncing.blockGap;
warpStatus = (
<span>, { first.mul(100).div(last).toFormat(2) }% historic</span>
);
}
let syncStatus = null;
if (syncing && syncing.currentBlock && syncing.highestBlock) {
syncStatus = (
<span>{ syncing.currentBlock.toFormat() }/{ syncing.highestBlock.toFormat() } syncing</span>
<span>
<FormattedMessage
id='ui.blockStatus.warpStatus'
defaultMessage=', {percentage}% historic'
values={ {
percentage: first.mul(100).div(last).toFormat(2)
} } />
</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';
function renderShallow (props) {
function render (props = {}) {
if (props && props.label === undefined) {
props.label = 'test';
}
return shallow(
<Button { ...props } />
);
@ -28,11 +32,11 @@ function renderShallow (props) {
describe('ui/Button', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(renderShallow()).to.be.ok;
expect(render()).to.be.ok;
});
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 {
static propTypes = {
account: PropTypes.string.isRequired,
address: PropTypes.string.isRequired,
certifications: PropTypes.array.isRequired,
dappsUrl: PropTypes.string.isRequired
}
@ -60,10 +60,10 @@ class Certifications extends Component {
}
function mapStateToProps (_, initProps) {
const { account } = initProps;
const { address } = initProps;
return (state) => {
const certifications = state.certifications[account] || [];
const certifications = state.certifications[address] || [];
const dappsUrl = state.api.dappsUrl;
return { certifications, dappsUrl };

View File

@ -15,16 +15,27 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import ActionDone from 'material-ui/svg-icons/action/done';
import ContentClear from 'material-ui/svg-icons/content/clear';
import { FormattedMessage } from 'react-intl';
import { nodeOrStringProptype } from '~/util/proptypes';
import Button from '../Button';
import Modal from '../Modal';
import { CancelIcon, CheckIcon } from '../Icons';
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 {
static propTypes = {
children: PropTypes.node.isRequired,
@ -33,10 +44,10 @@ export default class ConfirmDialog extends Component {
iconDeny: PropTypes.node,
labelConfirm: PropTypes.string,
labelDeny: PropTypes.string,
title: nodeOrStringProptype().isRequired,
visible: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired,
onDeny: PropTypes.func.isRequired
onDeny: PropTypes.func.isRequired,
title: nodeOrStringProptype().isRequired,
visible: PropTypes.bool.isRequired
}
render () {
@ -60,12 +71,12 @@ export default class ConfirmDialog extends Component {
return [
<Button
label={ labelDeny || 'no' }
icon={ iconDeny || <ContentClear /> }
icon={ iconDeny || <CancelIcon /> }
label={ labelDeny || DEFAULT_NO }
onClick={ onDeny } />,
<Button
label={ labelConfirm || 'yes' }
icon={ iconConfirm || <ActionDone /> }
icon={ iconConfirm || <CheckIcon /> }
label={ labelConfirm || DEFAULT_YES }
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';
function renderShallow (props) {
function render (props) {
return shallow(
<Container { ...props } />
);
@ -28,11 +28,24 @@ function renderShallow (props) {
describe('ui/Container', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(renderShallow()).to.be.ok;
expect(render()).to.be.ok;
});
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
allowCopy: PropTypes.bool,
allowInput: PropTypes.bool,
className: PropTypes.string,
disabled: PropTypes.bool,
error: nodeOrStringProptype(),
hint: nodeOrStringProptype(),
@ -123,13 +124,14 @@ class AddressSelect extends Component {
renderInput () {
const { focused } = this.state;
const { accountsInfo, allowCopy, disabled, error, hint, label, readOnly, value } = this.props;
const { accountsInfo, allowCopy, className, disabled, error, hint, label, readOnly, value } = this.props;
const input = (
<InputAddress
accountsInfo={ accountsInfo }
allowCopy={ allowCopy }
disabled={ disabled }
className={ className }
disabled={ disabled || readOnly }
error={ error }
hint={ hint }
focused={ focused }
@ -215,8 +217,9 @@ class AddressSelect extends Component {
}
const { address, addressError } = validateAddress(inputValue);
const { registryValues } = this.store;
if (addressError) {
if (addressError || registryValues.length > 0) {
return null;
}

View File

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

View File

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

View File

@ -27,6 +27,7 @@ class InputAddressSelect extends Component {
contracts: PropTypes.object.isRequired,
allowCopy: PropTypes.bool,
className: PropTypes.string,
error: PropTypes.string,
hint: PropTypes.string,
label: PropTypes.string,
@ -36,13 +37,14 @@ class InputAddressSelect extends Component {
};
render () {
const { accounts, allowCopy, contacts, contracts, label, hint, error, value, onChange, readOnly } = this.props;
const { accounts, allowCopy, className, contacts, contracts, label, hint, error, value, onChange, readOnly } = this.props;
return (
<AddressSelect
allowCopy={ allowCopy }
allowInput
accounts={ accounts }
className={ className }
contacts={ contacts }
contracts={ contracts }
error={ error }

View File

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

View File

@ -29,6 +29,7 @@ const api = {
const store = {
estimated: '123',
histogram: {},
priceDefault: '456',
totalValue: '789',
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 CheckIcon from 'material-ui/svg-icons/navigation/check';
import CloseIcon from 'material-ui/svg-icons/navigation/close';
import CompareIcon from 'material-ui/svg-icons/action/compare-arrows';
import ComputerIcon from 'material-ui/svg-icons/hardware/desktop-mac';
import ContractIcon from 'material-ui/svg-icons/action/code';
import DashboardIcon from 'material-ui/svg-icons/action/dashboard';
import DeleteIcon from 'material-ui/svg-icons/action/delete';
import DoneIcon from 'material-ui/svg-icons/action/done-all';
import LockedIcon from 'material-ui/svg-icons/action/lock-outline';
import EditIcon from 'material-ui/svg-icons/content/create';
import LockedIcon from 'material-ui/svg-icons/action/lock';
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
import SaveIcon from 'material-ui/svg-icons/content/save';
import SendIcon from 'material-ui/svg-icons/content/send';
import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
import VerifyIcon from 'material-ui/svg-icons/action/verified-user';
import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock';
export {
AddIcon,
CancelIcon,
CheckIcon,
CloseIcon,
CompareIcon,
ComputerIcon,
ContractIcon,
DashboardIcon,
DeleteIcon,
DoneIcon,
EditIcon,
LockedIcon,
NextIcon,
PrevIcon,
SaveIcon,
SendIcon,
SnoozeIcon,
VisibleIcon
VerifyIcon,
VisibleIcon,
VpnIcon
};

View File

@ -34,8 +34,8 @@ class IdentityIcon extends Component {
button: PropTypes.bool,
center: PropTypes.bool,
className: PropTypes.string,
inline: PropTypes.bool,
images: PropTypes.object.isRequired,
inline: PropTypes.bool,
padded: 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/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { isNullAddress } from '~/util/validation';
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 {
static propTypes = {
@ -43,7 +53,7 @@ class IdentityName extends Component {
return null;
}
const nullName = isNullAddress(address) ? 'null' : null;
const nullName = isNullAddress(address) ? defaultNameNull : null;
const addressFallback = nullName || (shorten ? (<ShortenedHash data={ address } />) : address);
const fallback = unknown ? defaultName : addressFallback;
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
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { mount } from 'enzyme';
import React from 'react';
import { IntlProvider } from 'react-intl';
import sinon from 'sinon';
import IdentityName from './identityName';
@ -44,9 +46,11 @@ const STORE = {
function render (props) {
return mount(
<IdentityName
store={ STORE }
{ ...props } />
<IntlProvider locale='en'>
<IdentityName
store={ STORE }
{ ...props } />
</IntlProvider>
);
}
@ -74,7 +78,7 @@ describe('ui/IdentityName', () => {
});
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
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import CircularProgress from 'material-ui/CircularProgress';
import { Input, InputAddress } from '../Form';
import { TypedInput, InputAddress } from '../Form';
import MethodDecodingStore from './methodDecodingStore';
import styles from './methodDecoding.css';
@ -245,6 +244,7 @@ class MethodDecoding extends Component {
renderDeploy () {
const { historic, transaction } = this.props;
const { methodInputs } = this.state;
if (!historic) {
return (
@ -261,6 +261,14 @@ class MethodDecoding extends Component {
</div>
{ this.renderAddressName(transaction.creates, false) }
<div>
{ methodInputs && methodInputs.length ? 'with the following parameters:' : ''}
</div>
<div className={ styles.inputs }>
{ this.renderInputs() }
</div>
</div>
);
}
@ -364,39 +372,31 @@ class MethodDecoding extends Component {
renderInputs () {
const { methodInputs } = this.state;
return methodInputs.map((input, index) => {
switch (input.type) {
case 'address':
return (
<InputAddress
disabled
text
key={ index }
className={ styles.input }
value={ input.value }
label={ input.type } />
);
if (!methodInputs || methodInputs.length === 0) {
return null;
}
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) {
const { api } = this.context;
if (api.util.isInstanceOf(value, BigNumber)) {
return value.toFormat(0);
} else if (api.util.isArray(value)) {
if (api.util.isArray(value)) {
return api.util.bytesToHex(value);
}

View File

@ -18,6 +18,8 @@ import Contracts from '~/contracts';
import Abi from '~/abi';
import * as abis from '~/contracts/abi';
import { decodeMethodInput } from '~/api/util/decode';
const CONTRACT_CREATE = '0x60606040';
let instance = null;
@ -26,6 +28,8 @@ export default class MethodDecodingStore {
api = null;
_bytecodes = {};
_contractsAbi = {};
_isContract = {};
_methods = {};
@ -46,12 +50,17 @@ export default class MethodDecodingStore {
if (!contract || !contract.meta || !contract.meta.abi) {
return;
}
this.loadFromAbi(contract.meta.abi);
this.loadFromAbi(contract.meta.abi, contract.address);
});
}
loadFromAbi (_abi) {
loadFromAbi (_abi, contractAddress) {
const abi = new Abi(_abi);
if (contractAddress && abi) {
this._contractsAbi[contractAddress] = abi;
}
abi
.functions
.map((f) => ({ sign: f.signature, abi: f.abi }))
@ -111,6 +120,7 @@ export default class MethodDecodingStore {
const contractAddress = isReceived ? transaction.from : transaction.to;
const input = transaction.input || transaction.data;
result.input = input;
result.received = isReceived;
// No input, should be a ETH transfer
@ -118,17 +128,20 @@ export default class MethodDecodingStore {
return Promise.resolve(result);
}
try {
const { signature } = this.api.util.decodeCallData(input);
let signature;
if (signature === CONTRACT_CREATE || transaction.creates) {
result.contract = true;
return Promise.resolve({ ...result, deploy: true });
}
try {
const decodeCallDataResult = this.api.util.decodeCallData(input);
signature = decodeCallDataResult.signature;
} catch (e) {}
// Contract deployment
if (!signature || signature === CONTRACT_CREATE || transaction.creates) {
return this.decodeContractCreation(result, contractAddress || transaction.creates);
}
return this
.isContract(contractAddress || transaction.creates)
.isContract(contractAddress)
.then((isContract) => {
result.contract = isContract;
@ -140,11 +153,6 @@ export default class MethodDecodingStore {
result.signature = signature;
result.params = paramdata;
// Contract deployment
if (!signature) {
return Promise.resolve({ ...result, deploy: true });
}
return this
.fetchMethodAbi(signature)
.then((abi) => {
@ -173,6 +181,68 @@ export default class MethodDecodingStore {
});
}
decodeContractCreation (data, contractAddress) {
const result = {
...data,
contract: true,
deploy: true
};
const { input } = data;
const abi = this._contractsAbi[contractAddress];
if (!input || !abi || !abi.constructors || abi.constructors.length === 0) {
return Promise.resolve(result);
}
const constructorAbi = abi.constructors[0];
const rawInput = /^(?:0x)?(.*)$/.exec(input)[1];
return this
.getCode(contractAddress)
.then((code) => {
if (!code || /^(0x)0*?$/.test(code)) {
return result;
}
const rawCode = /^(?:0x)?(.*)$/.exec(code)[1];
const codeOffset = rawInput.indexOf(rawCode);
if (codeOffset === -1) {
return result;
}
// Params are the last bytes of the transaction Input
// (minus the bytecode). It seems that they are repeated
// twice
const params = rawInput.slice(codeOffset + rawCode.length);
const paramsBis = params.slice(params.length / 2);
let decodedInputs;
try {
decodedInputs = decodeMethodInput(constructorAbi, params);
} catch (e) {}
try {
if (!decodedInputs) {
decodedInputs = decodeMethodInput(constructorAbi, paramsBis);
}
} catch (e) {}
if (decodedInputs && decodedInputs.length > 0) {
result.inputs = decodedInputs
.map((value, index) => {
const type = constructorAbi.inputs[index].kind.type;
return { type, value };
});
}
return result;
});
}
fetchMethodAbi (signature) {
if (this._methods[signature] !== undefined) {
return Promise.resolve(this._methods[signature]);
@ -209,7 +279,7 @@ export default class MethodDecodingStore {
return Promise.resolve(this._isContract[contractAddress]);
}
this._isContract[contractAddress] = this.api.eth
this._isContract[contractAddress] = this
.getCode(contractAddress)
.then((bytecode) => {
// Is a contract if the address contains *valid* bytecode
@ -222,4 +292,24 @@ export default class MethodDecodingStore {
return Promise.resolve(this._isContract[contractAddress]);
}
getCode (contractAddress) {
// If zero address, resolve to '0x'
if (!contractAddress || /^(0x)?0*$/.test(contractAddress)) {
return Promise.resolve('0x');
}
if (this._bytecodes[contractAddress]) {
return Promise.resolve(this._bytecodes[contractAddress]);
}
this._bytecodes[contractAddress] = this.api.eth
.getCode(contractAddress)
.then((bytecode) => {
this._bytecodes[contractAddress] = bytecode;
return this._bytecodes[contractAddress];
});
return Promise.resolve(this._bytecodes[contractAddress]);
}
}

View File

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

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() });
function renderShallow (props) {
function render (props) {
return shallow(
<TxRow
{ ...props } />,
@ -33,7 +33,7 @@ function renderShallow (props) {
);
}
describe('ui/TxRow', () => {
describe('ui/TxList/TxRow', () => {
describe('rendering', () => {
it('renders defaults', () => {
const block = {
@ -45,7 +45,7 @@ describe('ui/TxRow', () => {
value: new BigNumber(1)
};
expect(renderShallow({ block, tx })).to.be.ok;
expect(render({ address: '0x123', block, isTest: true, tx })).to.be.ok;
});
});
});

View File

@ -36,7 +36,7 @@ const STORE = {
}
};
function renderShallow (props) {
function render (props) {
return shallow(
<TxList
store={ STORE }
@ -48,7 +48,7 @@ function renderShallow (props) {
describe('ui/TxList', () => {
describe('rendering', () => {
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/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags } from '~/ui';
import CopyToClipboard from '~/ui/CopyToClipboard';
@ -26,50 +27,45 @@ export default class Header extends Component {
static propTypes = {
account: PropTypes.object,
balance: PropTypes.object,
className: PropTypes.string,
children: PropTypes.node,
isContract: PropTypes.bool,
hideName: PropTypes.bool
className: PropTypes.string,
hideName: PropTypes.bool,
isContract: PropTypes.bool
};
static defaultProps = {
className: '',
children: null,
isContract: false,
hideName: false
className: '',
hideName: false,
isContract: false
};
render () {
const { account, balance, className, children, hideName } = this.props;
const { address, meta, uuid } = account;
const { account, balance, children, className, hideName } = this.props;
if (!account) {
return null;
}
const uuidText = !uuid
? null
: <div className={ styles.uuidline }>uuid: { uuid }</div>;
const { address } = account;
const meta = account.meta || {};
return (
<div className={ className }>
<Container>
<IdentityIcon
address={ address } />
<IdentityIcon address={ address } />
<div className={ styles.floatleft }>
{ this.renderName(address) }
{ this.renderName() }
<div className={ [ hideName ? styles.bigaddress : '', styles.addressline ].join(' ') }>
<CopyToClipboard data={ address } />
<div className={ styles.address }>{ address }</div>
</div>
{ uuidText }
{ this.renderUuid() }
<div className={ styles.infoline }>
{ meta.description }
</div>
{ this.renderTxCount() }
</div>
<div className={ styles.tags }>
<Tags tags={ meta.tags } />
</div>
@ -77,9 +73,7 @@ export default class Header extends Component {
<Balance
account={ account }
balance={ balance } />
<Certifications
account={ account.address }
/>
<Certifications address={ address } />
</div>
{ children }
</Container>
@ -87,15 +81,22 @@ export default class Header extends Component {
);
}
renderName (address) {
renderName () {
const { hideName } = this.props;
if (hideName) {
return null;
}
const { address } = this.props.account;
return (
<ContainerTitle title={ <IdentityName address={ address } unknown /> } />
<ContainerTitle
title={
<IdentityName
address={ address }
unknown />
} />
);
}
@ -114,7 +115,31 @@ export default class Header extends Component {
return (
<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>
);
}

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

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

View File

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

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

View File

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

View File

@ -48,17 +48,43 @@ export default class DappsStore {
this.readDisplayApps();
this.loadExternalOverlay();
this.loadApps();
this.subscribeToChanges();
}
loadApps () {
/**
* Try to find the app from the local (local or builtin)
* apps, else fetch from the node
*/
loadApp (id) {
const { dappReg } = Contracts.get();
Promise
return this
.loadLocalApps()
.then(() => {
const app = this.apps.find((app) => app.id === id);
if (app) {
return app;
}
return this.fetchRegistryApp(dappReg, id, true);
});
}
loadLocalApps () {
return Promise
.all([
this.fetchBuiltinApps().then((apps) => this.addApps(apps)),
this.fetchLocalApps().then((apps) => this.addApps(apps)),
this.fetchLocalApps().then((apps) => this.addApps(apps))
]);
}
loadAllApps () {
const { dappReg } = Contracts.get();
return Promise
.all([
this.loadLocalApps(),
this.fetchRegistryApps(dappReg).then((apps) => this.addApps(apps))
])
.then(this.writeDisplayApps);
@ -67,8 +93,6 @@ export default class DappsStore {
static get (api) {
if (!instance) {
instance = new DappsStore(api);
} else {
instance.loadApps();
}
return instance;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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