Merge branch 'master' into on-demand-les-request
This commit is contained in:
commit
8446a8354b
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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)",
|
||||
]
|
||||
|
@ -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()?;
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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() },
|
||||
|
@ -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.
|
||||
|
55
ethcore/src/types/basic_account.rs
Normal file
55
ethcore/src/types/basic_account.rs
Normal 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)?,
|
||||
})
|
||||
}
|
||||
}
|
@ -37,3 +37,4 @@ pub mod mode;
|
||||
pub mod pruning_info;
|
||||
pub mod security_level;
|
||||
pub mod encoded;
|
||||
pub mod basic_account;
|
||||
|
@ -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",
|
||||
|
6
js/src/3rdparty/etherscan/account.js
vendored
6
js/src/3rdparty/etherscan/account.js
vendored
@ -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
|
||||
};
|
||||
});
|
||||
|
38
js/src/3rdparty/etherscan/helpers.spec.js
vendored
Normal file
38
js/src/3rdparty/etherscan/helpers.spec.js
vendored
Normal 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
|
||||
};
|
18
js/src/3rdparty/shapeshift/helpers.spec.js
vendored
18
js/src/3rdparty/shapeshift/helpers.spec.js
vendored
@ -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
|
||||
};
|
||||
|
19
js/src/3rdparty/shapeshift/rpc.spec.js
vendored
19
js/src/3rdparty/shapeshift/rpc.spec.js
vendored
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
77
js/src/3rdparty/shapeshift/shapeshift.js
vendored
77
js/src/3rdparty/shapeshift/shapeshift.js
vendored
@ -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,
|
||||
|
110
js/src/3rdparty/shapeshift/shapeshift.spec.js
vendored
110
js/src/3rdparty/shapeshift/shapeshift.spec.js
vendored
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
import BigNumber from 'bignumber.js';
|
||||
import sinon from 'sinon';
|
||||
import 'sinon-as-promised';
|
||||
|
||||
import Eth from './eth';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
@ -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')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -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 () {
|
||||
|
@ -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'));
|
||||
});
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
});
|
||||
};
|
||||
|
@ -35,7 +35,12 @@
|
||||
.link {
|
||||
color: #00BCD4;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.link:hover {
|
||||
text-decoration: underline;
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -36,3 +36,7 @@
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
@ -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'));
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
@ -37,3 +37,6 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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 }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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));
|
||||
};
|
||||
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
|
37
js/src/dapps/registry/util/registry.js
Normal file
37
js/src/dapps/registry/util/registry.js
Normal 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);
|
||||
};
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
86
js/src/redux/providers/chainMiddleware.spec.js
Normal file
86
js/src/redux/providers/chainMiddleware.spec.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
@ -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
|
||||
};
|
||||
|
@ -50,8 +50,7 @@ export default class Actionbar extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolbarGroup
|
||||
className={ styles.toolbuttons }>
|
||||
<ToolbarGroup className={ styles.toolbuttons }>
|
||||
{ buttons }
|
||||
</ToolbarGroup>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
94
js/src/ui/BlockStatus/blockStatus.spec.js
Normal file
94
js/src/ui/BlockStatus/blockStatus.spec.js
Normal 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');
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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 };
|
||||
|
@ -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 } />
|
||||
];
|
||||
}
|
||||
|
157
js/src/ui/ConfirmDialog/confirmDialog.spec.js
Normal file
157
js/src/ui/ConfirmDialog/confirmDialog.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 }
|
||||
|
@ -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 }
|
||||
|
@ -29,6 +29,7 @@ const api = {
|
||||
|
||||
const store = {
|
||||
estimated: '123',
|
||||
histogram: {},
|
||||
priceDefault: '456',
|
||||
totalValue: '789',
|
||||
setGas: sinon.stub(),
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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
|
||||
}
|
||||
|
120
js/src/ui/IdentityIcon/identityIcon.spec.js
Normal file
120
js/src/ui/IdentityIcon/identityIcon.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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]);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
132
js/src/ui/TxHash/txHash.spec.js
Normal file
132
js/src/ui/TxHash/txHash.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
156
js/src/views/Account/Header/header.spec.js
Normal file
156
js/src/views/Account/Header/header.spec.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
118
js/src/views/Account/Transactions/store.js
Normal file
118
js/src/views/Account/Transactions/store.js
Normal 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
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
193
js/src/views/Account/Transactions/store.spec.js
Normal file
193
js/src/views/Account/Transactions/store.spec.js
Normal 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
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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) {
|
||||
|
55
js/src/views/Account/Transactions/transactions.spec.js
Normal file
55
js/src/views/Account/Transactions/transactions.spec.js
Normal 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/);
|
||||
});
|
||||
});
|
||||
});
|
31
js/src/views/Account/Transactions/transactions.test.js
Normal file
31
js/src/views/Account/Transactions/transactions.test.js
Normal 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
|
||||
};
|
@ -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);
|
||||
}
|
||||
|
||||
|
226
js/src/views/Account/account.spec.js
Normal file
226
js/src/views/Account/account.spec.js
Normal 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/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
52
js/src/views/Account/account.test.js
Normal file
52
js/src/views/Account/account.test.js
Normal 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
|
||||
};
|
50
js/src/views/Account/store.js
Normal file
50
js/src/views/Account/store.js
Normal 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;
|
||||
}
|
||||
}
|
84
js/src/views/Account/store.spec.js
Normal file
84
js/src/views/Account/store.spec.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -197,7 +197,7 @@ export default class Summary extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<Certifications account={ account.address } />
|
||||
<Certifications address={ account.address } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
156
js/src/views/Connection/connection.spec.js
Normal file
156
js/src/views/Connection/connection.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 } />
|
||||
|
@ -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';
|
||||
|
@ -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()),
|
||||
|
@ -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,
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user