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]]
|
[[package]]
|
||||||
name = "parity-ui-precompiled"
|
name = "parity-ui-precompiled"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
source = "git+https://github.com/ethcore/js-precompiled.git#ebea2bf78e076916b51b04d8b24187a6a85ae440"
|
source = "git+https://github.com/ethcore/js-precompiled.git#257b3ce8aaa6797507592200dc78b29b8a305c3f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
@ -150,6 +150,11 @@ impl AccountProvider {
|
|||||||
Ok(Address::from(address).into())
|
Ok(Address::from(address).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks whether an account with a given address is present.
|
||||||
|
pub fn has_account(&self, address: Address) -> Result<bool, Error> {
|
||||||
|
Ok(self.accounts()?.iter().any(|&a| a == address))
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns addresses of all accounts.
|
/// Returns addresses of all accounts.
|
||||||
pub fn accounts(&self) -> Result<Vec<Address>, Error> {
|
pub fn accounts(&self) -> Result<Vec<Address>, Error> {
|
||||||
let accounts = self.sstore.accounts()?;
|
let accounts = self.sstore.accounts()?;
|
||||||
|
@ -17,16 +17,17 @@
|
|||||||
//! Account state encoding and decoding
|
//! Account state encoding and decoding
|
||||||
|
|
||||||
use account_db::{AccountDB, AccountDBMut};
|
use account_db::{AccountDB, AccountDBMut};
|
||||||
|
use basic_account::BasicAccount;
|
||||||
use snapshot::Error;
|
use snapshot::Error;
|
||||||
|
|
||||||
use util::{U256, FixedHash, H256, Bytes, HashDB, SHA3_EMPTY, SHA3_NULL_RLP};
|
use util::{U256, FixedHash, H256, Bytes, HashDB, SHA3_EMPTY, SHA3_NULL_RLP};
|
||||||
use util::trie::{TrieDB, Trie};
|
use util::trie::{TrieDB, Trie};
|
||||||
use rlp::{Rlp, RlpStream, Stream, UntrustedRlp, View};
|
use rlp::{RlpStream, Stream, UntrustedRlp, View};
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
// An empty account -- these are replaced with RLP null data for a space optimization.
|
// An empty account -- these are replaced with RLP null data for a space optimization.
|
||||||
const ACC_EMPTY: Account = Account {
|
const ACC_EMPTY: BasicAccount = BasicAccount {
|
||||||
nonce: U256([0, 0, 0, 0]),
|
nonce: U256([0, 0, 0, 0]),
|
||||||
balance: U256([0, 0, 0, 0]),
|
balance: U256([0, 0, 0, 0]),
|
||||||
storage_root: SHA3_NULL_RLP,
|
storage_root: SHA3_NULL_RLP,
|
||||||
@ -59,48 +60,14 @@ impl CodeState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// An alternate account structure from ::account::Account.
|
// walk the account's storage trie, returning an RLP item containing the
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
// account properties and the storage.
|
||||||
pub struct Account {
|
pub fn to_fat_rlp(acc: &BasicAccount, acct_db: &AccountDB, used_code: &mut HashSet<H256>) -> Result<Bytes, Error> {
|
||||||
nonce: U256,
|
if acc == &ACC_EMPTY {
|
||||||
balance: U256,
|
|
||||||
storage_root: H256,
|
|
||||||
code_hash: H256,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Account {
|
|
||||||
// decode the account from rlp.
|
|
||||||
pub fn from_thin_rlp(rlp: &[u8]) -> Self {
|
|
||||||
let r: Rlp = Rlp::new(rlp);
|
|
||||||
|
|
||||||
Account {
|
|
||||||
nonce: r.val_at(0),
|
|
||||||
balance: r.val_at(1),
|
|
||||||
storage_root: r.val_at(2),
|
|
||||||
code_hash: r.val_at(3),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// encode the account to a standard rlp.
|
|
||||||
pub fn to_thin_rlp(&self) -> Bytes {
|
|
||||||
let mut stream = RlpStream::new_list(4);
|
|
||||||
stream
|
|
||||||
.append(&self.nonce)
|
|
||||||
.append(&self.balance)
|
|
||||||
.append(&self.storage_root)
|
|
||||||
.append(&self.code_hash);
|
|
||||||
|
|
||||||
stream.out()
|
|
||||||
}
|
|
||||||
|
|
||||||
// walk the account's storage trie, returning an RLP item containing the
|
|
||||||
// account properties and the storage.
|
|
||||||
pub fn to_fat_rlp(&self, acct_db: &AccountDB, used_code: &mut HashSet<H256>) -> Result<Bytes, Error> {
|
|
||||||
if self == &ACC_EMPTY {
|
|
||||||
return Ok(::rlp::NULL_RLP.to_vec());
|
return Ok(::rlp::NULL_RLP.to_vec());
|
||||||
}
|
}
|
||||||
|
|
||||||
let db = TrieDB::new(acct_db, &self.storage_root)?;
|
let db = TrieDB::new(acct_db, &acc.storage_root)?;
|
||||||
|
|
||||||
let mut pairs = Vec::new();
|
let mut pairs = Vec::new();
|
||||||
|
|
||||||
@ -118,18 +85,18 @@ impl Account {
|
|||||||
let pairs_rlp = stream.out();
|
let pairs_rlp = stream.out();
|
||||||
|
|
||||||
let mut account_stream = RlpStream::new_list(5);
|
let mut account_stream = RlpStream::new_list(5);
|
||||||
account_stream.append(&self.nonce)
|
account_stream.append(&acc.nonce)
|
||||||
.append(&self.balance);
|
.append(&acc.balance);
|
||||||
|
|
||||||
// [has_code, code_hash].
|
// [has_code, code_hash].
|
||||||
if self.code_hash == SHA3_EMPTY {
|
if acc.code_hash == SHA3_EMPTY {
|
||||||
account_stream.append(&CodeState::Empty.raw()).append_empty_data();
|
account_stream.append(&CodeState::Empty.raw()).append_empty_data();
|
||||||
} else if used_code.contains(&self.code_hash) {
|
} else if used_code.contains(&acc.code_hash) {
|
||||||
account_stream.append(&CodeState::Hash.raw()).append(&self.code_hash);
|
account_stream.append(&CodeState::Hash.raw()).append(&acc.code_hash);
|
||||||
} else {
|
} else {
|
||||||
match acct_db.get(&self.code_hash) {
|
match acct_db.get(&acc.code_hash) {
|
||||||
Some(c) => {
|
Some(c) => {
|
||||||
used_code.insert(self.code_hash.clone());
|
used_code.insert(acc.code_hash.clone());
|
||||||
account_stream.append(&CodeState::Inline.raw()).append(&&*c);
|
account_stream.append(&CodeState::Inline.raw()).append(&&*c);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@ -142,15 +109,15 @@ impl Account {
|
|||||||
account_stream.append_raw(&pairs_rlp, 1);
|
account_stream.append_raw(&pairs_rlp, 1);
|
||||||
|
|
||||||
Ok(account_stream.out())
|
Ok(account_stream.out())
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode a fat rlp, and rebuild the storage trie as we go.
|
// decode a fat rlp, and rebuild the storage trie as we go.
|
||||||
// returns the account structure along with its newly recovered code,
|
// returns the account structure along with its newly recovered code,
|
||||||
// if it exists.
|
// if it exists.
|
||||||
pub fn from_fat_rlp(
|
pub fn from_fat_rlp(
|
||||||
acct_db: &mut AccountDBMut,
|
acct_db: &mut AccountDBMut,
|
||||||
rlp: UntrustedRlp,
|
rlp: UntrustedRlp,
|
||||||
) -> Result<(Self, Option<Bytes>), Error> {
|
) -> Result<(BasicAccount, Option<Bytes>), Error> {
|
||||||
use util::{TrieDBMut, TrieMut};
|
use util::{TrieDBMut, TrieMut};
|
||||||
|
|
||||||
// check for special case of empty account.
|
// check for special case of empty account.
|
||||||
@ -194,7 +161,7 @@ impl Account {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let acc = Account {
|
let acc = BasicAccount {
|
||||||
nonce: nonce,
|
nonce: nonce,
|
||||||
balance: balance,
|
balance: balance,
|
||||||
storage_root: storage_root,
|
storage_root: storage_root,
|
||||||
@ -202,22 +169,12 @@ impl Account {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok((acc, new_code))
|
Ok((acc, new_code))
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the account's code hash.
|
|
||||||
pub fn code_hash(&self) -> &H256 {
|
|
||||||
&self.code_hash
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn storage_root_mut(&mut self) -> &mut H256 {
|
|
||||||
&mut self.storage_root
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use account_db::{AccountDB, AccountDBMut};
|
use account_db::{AccountDB, AccountDBMut};
|
||||||
|
use basic_account::BasicAccount;
|
||||||
use tests::helpers::get_temp_state_db;
|
use tests::helpers::get_temp_state_db;
|
||||||
use snapshot::tests::helpers::fill_storage;
|
use snapshot::tests::helpers::fill_storage;
|
||||||
|
|
||||||
@ -227,26 +184,26 @@ mod tests {
|
|||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use super::{ACC_EMPTY, Account};
|
use super::{ACC_EMPTY, to_fat_rlp, from_fat_rlp};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn encoding_basic() {
|
fn encoding_basic() {
|
||||||
let mut db = get_temp_state_db();
|
let mut db = get_temp_state_db();
|
||||||
let addr = Address::random();
|
let addr = Address::random();
|
||||||
|
|
||||||
let account = Account {
|
let account = BasicAccount {
|
||||||
nonce: 50.into(),
|
nonce: 50.into(),
|
||||||
balance: 123456789.into(),
|
balance: 123456789.into(),
|
||||||
storage_root: SHA3_NULL_RLP,
|
storage_root: SHA3_NULL_RLP,
|
||||||
code_hash: SHA3_EMPTY,
|
code_hash: SHA3_EMPTY,
|
||||||
};
|
};
|
||||||
|
|
||||||
let thin_rlp = account.to_thin_rlp();
|
let thin_rlp = ::rlp::encode(&account);
|
||||||
assert_eq!(Account::from_thin_rlp(&thin_rlp), account);
|
assert_eq!(::rlp::decode::<BasicAccount>(&thin_rlp), account);
|
||||||
|
|
||||||
let fat_rlp = account.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &addr), &mut Default::default()).unwrap();
|
let fat_rlp = to_fat_rlp(&account, &AccountDB::new(db.as_hashdb(), &addr), &mut Default::default()).unwrap();
|
||||||
let fat_rlp = UntrustedRlp::new(&fat_rlp);
|
let fat_rlp = UntrustedRlp::new(&fat_rlp);
|
||||||
assert_eq!(Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr), fat_rlp).unwrap().0, account);
|
assert_eq!(from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr), fat_rlp).unwrap().0, account);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -258,7 +215,7 @@ mod tests {
|
|||||||
let acct_db = AccountDBMut::new(db.as_hashdb_mut(), &addr);
|
let acct_db = AccountDBMut::new(db.as_hashdb_mut(), &addr);
|
||||||
let mut root = SHA3_NULL_RLP;
|
let mut root = SHA3_NULL_RLP;
|
||||||
fill_storage(acct_db, &mut root, &mut H256::zero());
|
fill_storage(acct_db, &mut root, &mut H256::zero());
|
||||||
Account {
|
BasicAccount {
|
||||||
nonce: 25.into(),
|
nonce: 25.into(),
|
||||||
balance: 987654321.into(),
|
balance: 987654321.into(),
|
||||||
storage_root: root,
|
storage_root: root,
|
||||||
@ -266,12 +223,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let thin_rlp = account.to_thin_rlp();
|
let thin_rlp = ::rlp::encode(&account);
|
||||||
assert_eq!(Account::from_thin_rlp(&thin_rlp), account);
|
assert_eq!(::rlp::decode::<BasicAccount>(&thin_rlp), account);
|
||||||
|
|
||||||
let fat_rlp = account.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &addr), &mut Default::default()).unwrap();
|
let fat_rlp = to_fat_rlp(&account, &AccountDB::new(db.as_hashdb(), &addr), &mut Default::default()).unwrap();
|
||||||
let fat_rlp = UntrustedRlp::new(&fat_rlp);
|
let fat_rlp = UntrustedRlp::new(&fat_rlp);
|
||||||
assert_eq!(Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr), fat_rlp).unwrap().0, account);
|
assert_eq!(from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr), fat_rlp).unwrap().0, account);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -291,14 +248,14 @@ mod tests {
|
|||||||
acct_db.emplace(code_hash.clone(), DBValue::from_slice(b"this is definitely code"));
|
acct_db.emplace(code_hash.clone(), DBValue::from_slice(b"this is definitely code"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let account1 = Account {
|
let account1 = BasicAccount {
|
||||||
nonce: 50.into(),
|
nonce: 50.into(),
|
||||||
balance: 123456789.into(),
|
balance: 123456789.into(),
|
||||||
storage_root: SHA3_NULL_RLP,
|
storage_root: SHA3_NULL_RLP,
|
||||||
code_hash: code_hash,
|
code_hash: code_hash,
|
||||||
};
|
};
|
||||||
|
|
||||||
let account2 = Account {
|
let account2 = BasicAccount {
|
||||||
nonce: 400.into(),
|
nonce: 400.into(),
|
||||||
balance: 98765432123456789usize.into(),
|
balance: 98765432123456789usize.into(),
|
||||||
storage_root: SHA3_NULL_RLP,
|
storage_root: SHA3_NULL_RLP,
|
||||||
@ -307,18 +264,18 @@ mod tests {
|
|||||||
|
|
||||||
let mut used_code = HashSet::new();
|
let mut used_code = HashSet::new();
|
||||||
|
|
||||||
let fat_rlp1 = account1.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &addr1), &mut used_code).unwrap();
|
let fat_rlp1 = to_fat_rlp(&account1, &AccountDB::new(db.as_hashdb(), &addr1), &mut used_code).unwrap();
|
||||||
let fat_rlp2 = account2.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &addr2), &mut used_code).unwrap();
|
let fat_rlp2 = to_fat_rlp(&account2, &AccountDB::new(db.as_hashdb(), &addr2), &mut used_code).unwrap();
|
||||||
assert_eq!(used_code.len(), 1);
|
assert_eq!(used_code.len(), 1);
|
||||||
|
|
||||||
let fat_rlp1 = UntrustedRlp::new(&fat_rlp1);
|
let fat_rlp1 = UntrustedRlp::new(&fat_rlp1);
|
||||||
let fat_rlp2 = UntrustedRlp::new(&fat_rlp2);
|
let fat_rlp2 = UntrustedRlp::new(&fat_rlp2);
|
||||||
|
|
||||||
let (acc, maybe_code) = Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr2), fat_rlp2).unwrap();
|
let (acc, maybe_code) = from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr2), fat_rlp2).unwrap();
|
||||||
assert!(maybe_code.is_none());
|
assert!(maybe_code.is_none());
|
||||||
assert_eq!(acc, account2);
|
assert_eq!(acc, account2);
|
||||||
|
|
||||||
let (acc, maybe_code) = Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr1), fat_rlp1).unwrap();
|
let (acc, maybe_code) = from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr1), fat_rlp1).unwrap();
|
||||||
assert_eq!(maybe_code, Some(b"this is definitely code".to_vec()));
|
assert_eq!(maybe_code, Some(b"this is definitely code".to_vec()));
|
||||||
assert_eq!(acc, account1);
|
assert_eq!(acc, account1);
|
||||||
}
|
}
|
||||||
@ -328,7 +285,7 @@ mod tests {
|
|||||||
let mut db = get_temp_state_db();
|
let mut db = get_temp_state_db();
|
||||||
let mut used_code = HashSet::new();
|
let mut used_code = HashSet::new();
|
||||||
|
|
||||||
assert_eq!(ACC_EMPTY.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &Address::default()), &mut used_code).unwrap(), ::rlp::NULL_RLP.to_vec());
|
assert_eq!(to_fat_rlp(&ACC_EMPTY, &AccountDB::new(db.as_hashdb(), &Address::default()), &mut used_code).unwrap(), ::rlp::NULL_RLP.to_vec());
|
||||||
assert_eq!(Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &Address::default()), UntrustedRlp::new(&::rlp::NULL_RLP)).unwrap(), (ACC_EMPTY, None));
|
assert_eq!(from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &Address::default()), UntrustedRlp::new(&::rlp::NULL_RLP)).unwrap(), (ACC_EMPTY, None));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,6 @@ use util::sha3::SHA3_NULL_RLP;
|
|||||||
use rlp::{RlpStream, Stream, UntrustedRlp, View};
|
use rlp::{RlpStream, Stream, UntrustedRlp, View};
|
||||||
use bloom_journal::Bloom;
|
use bloom_journal::Bloom;
|
||||||
|
|
||||||
use self::account::Account;
|
|
||||||
use self::block::AbridgedBlock;
|
use self::block::AbridgedBlock;
|
||||||
use self::io::SnapshotWriter;
|
use self::io::SnapshotWriter;
|
||||||
|
|
||||||
@ -368,12 +367,12 @@ pub fn chunk_state<'a>(db: &HashDB, root: &H256, writer: &Mutex<SnapshotWriter +
|
|||||||
// account_key here is the address' hash.
|
// account_key here is the address' hash.
|
||||||
for item in account_trie.iter()? {
|
for item in account_trie.iter()? {
|
||||||
let (account_key, account_data) = item?;
|
let (account_key, account_data) = item?;
|
||||||
let account = Account::from_thin_rlp(&*account_data);
|
let account = ::rlp::decode(&*account_data);
|
||||||
let account_key_hash = H256::from_slice(&account_key);
|
let account_key_hash = H256::from_slice(&account_key);
|
||||||
|
|
||||||
let account_db = AccountDB::from_hash(db, account_key_hash);
|
let account_db = AccountDB::from_hash(db, account_key_hash);
|
||||||
|
|
||||||
let fat_rlp = account.to_fat_rlp(&account_db, &mut used_code)?;
|
let fat_rlp = account::to_fat_rlp(&account, &account_db, &mut used_code)?;
|
||||||
chunker.push(account_key, fat_rlp)?;
|
chunker.push(account_key, fat_rlp)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -507,10 +506,10 @@ fn rebuild_accounts(
|
|||||||
// fill out the storage trie and code while decoding.
|
// fill out the storage trie and code while decoding.
|
||||||
let (acc, maybe_code) = {
|
let (acc, maybe_code) = {
|
||||||
let mut acct_db = AccountDBMut::from_hash(db, hash);
|
let mut acct_db = AccountDBMut::from_hash(db, hash);
|
||||||
Account::from_fat_rlp(&mut acct_db, fat_rlp)?
|
account::from_fat_rlp(&mut acct_db, fat_rlp)?
|
||||||
};
|
};
|
||||||
|
|
||||||
let code_hash = acc.code_hash().clone();
|
let code_hash = acc.code_hash.clone();
|
||||||
match maybe_code {
|
match maybe_code {
|
||||||
// new inline code
|
// new inline code
|
||||||
Some(code) => status.new_code.push((code_hash, code, hash)),
|
Some(code) => status.new_code.push((code_hash, code, hash)),
|
||||||
@ -534,7 +533,7 @@ fn rebuild_accounts(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
acc.to_thin_rlp()
|
::rlp::encode(&acc).to_vec()
|
||||||
};
|
};
|
||||||
|
|
||||||
*out = (hash, thin_rlp);
|
*out = (hash, thin_rlp);
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
//! Snapshot test helpers. These are used to build blockchains and state tries
|
//! Snapshot test helpers. These are used to build blockchains and state tries
|
||||||
//! which can be queried before and after a full snapshot/restore cycle.
|
//! which can be queried before and after a full snapshot/restore cycle.
|
||||||
|
|
||||||
|
use basic_account::BasicAccount;
|
||||||
use account_db::AccountDBMut;
|
use account_db::AccountDBMut;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use snapshot::account::Account;
|
|
||||||
|
|
||||||
use util::DBValue;
|
use util::DBValue;
|
||||||
use util::hash::{FixedHash, H256};
|
use util::hash::{FixedHash, H256};
|
||||||
@ -64,10 +64,10 @@ impl StateProducer {
|
|||||||
|
|
||||||
// sweep once to alter storage tries.
|
// sweep once to alter storage tries.
|
||||||
for &mut (ref mut address_hash, ref mut account_data) in &mut accounts_to_modify {
|
for &mut (ref mut address_hash, ref mut account_data) in &mut accounts_to_modify {
|
||||||
let mut account = Account::from_thin_rlp(&*account_data);
|
let mut account: BasicAccount = ::rlp::decode(&*account_data);
|
||||||
let acct_db = AccountDBMut::from_hash(db, *address_hash);
|
let acct_db = AccountDBMut::from_hash(db, *address_hash);
|
||||||
fill_storage(acct_db, account.storage_root_mut(), &mut self.storage_seed);
|
fill_storage(acct_db, &mut account.storage_root, &mut self.storage_seed);
|
||||||
*account_data = DBValue::from_vec(account.to_thin_rlp());
|
*account_data = DBValue::from_vec(::rlp::encode(&account).to_vec());
|
||||||
}
|
}
|
||||||
|
|
||||||
// sweep again to alter account trie.
|
// sweep again to alter account trie.
|
||||||
|
@ -16,8 +16,9 @@
|
|||||||
|
|
||||||
//! State snapshotting tests.
|
//! State snapshotting tests.
|
||||||
|
|
||||||
|
use basic_account::BasicAccount;
|
||||||
|
use snapshot::account;
|
||||||
use snapshot::{chunk_state, Error as SnapshotError, Progress, StateRebuilder};
|
use snapshot::{chunk_state, Error as SnapshotError, Progress, StateRebuilder};
|
||||||
use snapshot::account::Account;
|
|
||||||
use snapshot::io::{PackedReader, PackedWriter, SnapshotReader, SnapshotWriter};
|
use snapshot::io::{PackedReader, PackedWriter, SnapshotReader, SnapshotWriter};
|
||||||
use super::helpers::{compare_dbs, StateProducer};
|
use super::helpers::{compare_dbs, StateProducer};
|
||||||
|
|
||||||
@ -113,22 +114,21 @@ fn get_code_from_prev_chunk() {
|
|||||||
// first one will have code inlined,
|
// first one will have code inlined,
|
||||||
// second will just have its hash.
|
// second will just have its hash.
|
||||||
let thin_rlp = acc_stream.out();
|
let thin_rlp = acc_stream.out();
|
||||||
let acc1 = Account::from_thin_rlp(&thin_rlp);
|
let acc: BasicAccount = ::rlp::decode(&thin_rlp);
|
||||||
let acc2 = Account::from_thin_rlp(&thin_rlp);
|
|
||||||
|
|
||||||
let mut make_chunk = |acc: Account, hash| {
|
let mut make_chunk = |acc, hash| {
|
||||||
let mut db = MemoryDB::new();
|
let mut db = MemoryDB::new();
|
||||||
AccountDBMut::from_hash(&mut db, hash).insert(&code[..]);
|
AccountDBMut::from_hash(&mut db, hash).insert(&code[..]);
|
||||||
|
|
||||||
let fat_rlp = acc.to_fat_rlp(&AccountDB::from_hash(&db, hash), &mut used_code).unwrap();
|
let fat_rlp = account::to_fat_rlp(&acc, &AccountDB::from_hash(&db, hash), &mut used_code).unwrap();
|
||||||
|
|
||||||
let mut stream = RlpStream::new_list(1);
|
let mut stream = RlpStream::new_list(1);
|
||||||
stream.begin_list(2).append(&hash).append_raw(&fat_rlp, 1);
|
stream.begin_list(2).append(&hash).append_raw(&fat_rlp, 1);
|
||||||
stream.out()
|
stream.out()
|
||||||
};
|
};
|
||||||
|
|
||||||
let chunk1 = make_chunk(acc1, h1);
|
let chunk1 = make_chunk(acc.clone(), h1);
|
||||||
let chunk2 = make_chunk(acc2, h2);
|
let chunk2 = make_chunk(acc, h2);
|
||||||
|
|
||||||
let db_path = RandomTempPath::create_dir();
|
let db_path = RandomTempPath::create_dir();
|
||||||
let db_cfg = DatabaseConfig::with_columns(::db::NUM_COLUMNS);
|
let db_cfg = DatabaseConfig::with_columns(::db::NUM_COLUMNS);
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use util::{Address, H256, Uint, U256};
|
use util::{Address, H256, Uint, U256, FixedHash};
|
||||||
use util::sha3::SHA3_NULL_RLP;
|
use util::sha3::SHA3_NULL_RLP;
|
||||||
use ethjson;
|
use ethjson;
|
||||||
use super::seal::Seal;
|
use super::seal::Seal;
|
||||||
@ -50,9 +50,9 @@ impl From<ethjson::spec::Genesis> for Genesis {
|
|||||||
Genesis {
|
Genesis {
|
||||||
seal: From::from(g.seal),
|
seal: From::from(g.seal),
|
||||||
difficulty: g.difficulty.into(),
|
difficulty: g.difficulty.into(),
|
||||||
author: g.author.into(),
|
author: g.author.map_or_else(Address::zero, Into::into),
|
||||||
timestamp: g.timestamp.into(),
|
timestamp: g.timestamp.map_or(0, Into::into),
|
||||||
parent_hash: g.parent_hash.into(),
|
parent_hash: g.parent_hash.map_or_else(H256::zero, Into::into),
|
||||||
gas_limit: g.gas_limit.into(),
|
gas_limit: g.gas_limit.into(),
|
||||||
transactions_root: g.transactions_root.map_or_else(|| SHA3_NULL_RLP.clone(), Into::into),
|
transactions_root: g.transactions_root.map_or_else(|| SHA3_NULL_RLP.clone(), Into::into),
|
||||||
receipts_root: g.receipts_root.map_or_else(|| SHA3_NULL_RLP.clone(), Into::into),
|
receipts_root: g.receipts_root.map_or_else(|| SHA3_NULL_RLP.clone(), Into::into),
|
||||||
|
@ -58,7 +58,7 @@ pub struct CommonParams {
|
|||||||
impl From<ethjson::spec::Params> for CommonParams {
|
impl From<ethjson::spec::Params> for CommonParams {
|
||||||
fn from(p: ethjson::spec::Params) -> Self {
|
fn from(p: ethjson::spec::Params) -> Self {
|
||||||
CommonParams {
|
CommonParams {
|
||||||
account_start_nonce: p.account_start_nonce.into(),
|
account_start_nonce: p.account_start_nonce.map_or_else(U256::zero, Into::into),
|
||||||
maximum_extra_data_size: p.maximum_extra_data_size.into(),
|
maximum_extra_data_size: p.maximum_extra_data_size.into(),
|
||||||
network_id: p.network_id.into(),
|
network_id: p.network_id.into(),
|
||||||
chain_id: if let Some(n) = p.chain_id { n.into() } else { p.network_id.into() },
|
chain_id: if let Some(n) = p.chain_id { n.into() } else { p.network_id.into() },
|
||||||
|
@ -20,6 +20,7 @@ use util::*;
|
|||||||
use pod_account::*;
|
use pod_account::*;
|
||||||
use rlp::*;
|
use rlp::*;
|
||||||
use lru_cache::LruCache;
|
use lru_cache::LruCache;
|
||||||
|
use basic_account::BasicAccount;
|
||||||
|
|
||||||
use std::cell::{RefCell, Cell};
|
use std::cell::{RefCell, Cell};
|
||||||
|
|
||||||
@ -53,6 +54,23 @@ pub struct Account {
|
|||||||
address_hash: Cell<Option<H256>>,
|
address_hash: Cell<Option<H256>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<BasicAccount> for Account {
|
||||||
|
fn from(basic: BasicAccount) -> Self {
|
||||||
|
Account {
|
||||||
|
balance: basic.balance,
|
||||||
|
nonce: basic.nonce,
|
||||||
|
storage_root: basic.storage_root,
|
||||||
|
storage_cache: Self::empty_storage_cache(),
|
||||||
|
storage_changes: HashMap::new(),
|
||||||
|
code_hash: basic.code_hash,
|
||||||
|
code_size: None,
|
||||||
|
code_cache: Arc::new(vec![]),
|
||||||
|
code_filth: Filth::Clean,
|
||||||
|
address_hash: Cell::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Account {
|
impl Account {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
/// General constructor.
|
/// General constructor.
|
||||||
@ -109,19 +127,8 @@ impl Account {
|
|||||||
|
|
||||||
/// Create a new account from RLP.
|
/// Create a new account from RLP.
|
||||||
pub fn from_rlp(rlp: &[u8]) -> Account {
|
pub fn from_rlp(rlp: &[u8]) -> Account {
|
||||||
let r: Rlp = Rlp::new(rlp);
|
let basic: BasicAccount = ::rlp::decode(rlp);
|
||||||
Account {
|
basic.into()
|
||||||
nonce: r.val_at(0),
|
|
||||||
balance: r.val_at(1),
|
|
||||||
storage_root: r.val_at(2),
|
|
||||||
storage_cache: Self::empty_storage_cache(),
|
|
||||||
storage_changes: HashMap::new(),
|
|
||||||
code_hash: r.val_at(3),
|
|
||||||
code_cache: Arc::new(vec![]),
|
|
||||||
code_size: None,
|
|
||||||
code_filth: Filth::Clean,
|
|
||||||
address_hash: Cell::new(None),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new contract account.
|
/// Create a new contract account.
|
||||||
|
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 pruning_info;
|
||||||
pub mod security_level;
|
pub mod security_level;
|
||||||
pub mod encoded;
|
pub mod encoded;
|
||||||
|
pub mod basic_account;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "parity.js",
|
"name": "parity.js",
|
||||||
"version": "0.2.165",
|
"version": "0.2.168",
|
||||||
"main": "release/index.js",
|
"main": "release/index.js",
|
||||||
"jsnext:main": "src/index.js",
|
"jsnext:main": "src/index.js",
|
||||||
"author": "Parity Team <admin@parity.io>",
|
"author": "Parity Team <admin@parity.io>",
|
||||||
@ -43,8 +43,8 @@
|
|||||||
"lint:css": "stylelint ./src/**/*.css",
|
"lint:css": "stylelint ./src/**/*.css",
|
||||||
"lint:js": "eslint --ignore-path .gitignore ./src/",
|
"lint:js": "eslint --ignore-path .gitignore ./src/",
|
||||||
"lint:js:cached": "eslint --cache --ignore-path .gitignore ./src/",
|
"lint:js:cached": "eslint --cache --ignore-path .gitignore ./src/",
|
||||||
"test": "NODE_ENV=test mocha 'src/**/*.spec.js'",
|
"test": "NODE_ENV=test mocha --compilers ejs:ejsify 'src/**/*.spec.js'",
|
||||||
"test:coverage": "NODE_ENV=test istanbul cover _mocha -- 'src/**/*.spec.js'",
|
"test:coverage": "NODE_ENV=test istanbul cover _mocha -- --compilers ejs:ejsify 'src/**/*.spec.js'",
|
||||||
"test:e2e": "NODE_ENV=test mocha 'src/**/*.e2e.js'",
|
"test:e2e": "NODE_ENV=test mocha 'src/**/*.e2e.js'",
|
||||||
"test:npm": "(cd .npmjs && npm i) && node test/npmParity && (rm -rf .npmjs/node_modules)",
|
"test:npm": "(cd .npmjs && npm i) && node test/npmParity && (rm -rf .npmjs/node_modules)",
|
||||||
"prepush": "npm run lint:cached"
|
"prepush": "npm run lint:cached"
|
||||||
@ -80,6 +80,7 @@
|
|||||||
"coveralls": "2.11.15",
|
"coveralls": "2.11.15",
|
||||||
"css-loader": "0.26.1",
|
"css-loader": "0.26.1",
|
||||||
"ejs-loader": "0.3.0",
|
"ejs-loader": "0.3.0",
|
||||||
|
"ejsify": "1.0.0",
|
||||||
"enzyme": "2.7.0",
|
"enzyme": "2.7.0",
|
||||||
"eslint": "3.11.1",
|
"eslint": "3.11.1",
|
||||||
"eslint-config-semistandard": "7.0.0",
|
"eslint-config-semistandard": "7.0.0",
|
||||||
@ -138,6 +139,7 @@
|
|||||||
"blockies": "0.0.2",
|
"blockies": "0.0.2",
|
||||||
"brace": "0.9.0",
|
"brace": "0.9.0",
|
||||||
"bytes": "2.4.0",
|
"bytes": "2.4.0",
|
||||||
|
"crypto-js": "3.1.9-1",
|
||||||
"debounce": "1.0.0",
|
"debounce": "1.0.0",
|
||||||
"es6-error": "4.0.0",
|
"es6-error": "4.0.0",
|
||||||
"es6-promise": "4.0.5",
|
"es6-promise": "4.0.5",
|
||||||
|
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
|
// page offset from 0
|
||||||
return _call('txlist', {
|
return _call('txlist', {
|
||||||
address: address,
|
address: address,
|
||||||
page: (page || 0) + 1,
|
|
||||||
offset: PAGE_SIZE,
|
offset: PAGE_SIZE,
|
||||||
|
page: (page || 0) + 1,
|
||||||
sort: 'desc'
|
sort: 'desc'
|
||||||
}, test).then((transactions) => {
|
}, test).then((transactions) => {
|
||||||
return transactions.map((tx) => {
|
return transactions.map((tx) => {
|
||||||
return {
|
return {
|
||||||
|
blockNumber: new BigNumber(tx.blockNumber || 0),
|
||||||
from: util.toChecksumAddress(tx.from),
|
from: util.toChecksumAddress(tx.from),
|
||||||
to: util.toChecksumAddress(tx.to),
|
|
||||||
hash: tx.hash,
|
hash: tx.hash,
|
||||||
blockNumber: new BigNumber(tx.blockNumber),
|
|
||||||
timeStamp: tx.timeStamp,
|
timeStamp: tx.timeStamp,
|
||||||
|
to: util.toChecksumAddress(tx.to),
|
||||||
value: tx.value
|
value: tx.value
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
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 nock = require('nock');
|
||||||
|
|
||||||
const ShapeShift = require('./');
|
|
||||||
const initShapeshift = (ShapeShift.default || ShapeShift);
|
|
||||||
|
|
||||||
const APIKEY = '0x123454321';
|
const APIKEY = '0x123454321';
|
||||||
|
|
||||||
const shapeshift = initShapeshift(APIKEY);
|
function mockget (shapeshift, requests) {
|
||||||
const rpc = shapeshift.getRpc();
|
let scope = nock(shapeshift.getRpc().ENDPOINT);
|
||||||
|
|
||||||
function mockget (requests) {
|
|
||||||
let scope = nock(rpc.ENDPOINT);
|
|
||||||
|
|
||||||
requests.forEach((request) => {
|
requests.forEach((request) => {
|
||||||
scope = scope
|
scope = scope
|
||||||
@ -38,8 +32,8 @@ function mockget (requests) {
|
|||||||
return scope;
|
return scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mockpost (requests) {
|
function mockpost (shapeshift, requests) {
|
||||||
let scope = nock(rpc.ENDPOINT);
|
let scope = nock(shapeshift.getRpc().ENDPOINT);
|
||||||
|
|
||||||
requests.forEach((request) => {
|
requests.forEach((request) => {
|
||||||
scope = scope
|
scope = scope
|
||||||
@ -58,7 +52,5 @@ function mockpost (requests) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
APIKEY,
|
APIKEY,
|
||||||
mockget,
|
mockget,
|
||||||
mockpost,
|
mockpost
|
||||||
shapeshift,
|
|
||||||
rpc
|
|
||||||
};
|
};
|
||||||
|
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 helpers = require('./helpers.spec.js');
|
||||||
|
|
||||||
const APIKEY = helpers.APIKEY;
|
const ShapeShift = require('./');
|
||||||
|
const initShapeshift = (ShapeShift.default || ShapeShift);
|
||||||
|
|
||||||
const mockget = helpers.mockget;
|
const mockget = helpers.mockget;
|
||||||
const mockpost = helpers.mockpost;
|
const mockpost = helpers.mockpost;
|
||||||
const rpc = helpers.rpc;
|
|
||||||
|
|
||||||
describe('shapeshift/rpc', () => {
|
describe('shapeshift/rpc', () => {
|
||||||
|
let rpc;
|
||||||
|
let shapeshift;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
shapeshift = initShapeshift(helpers.APIKEY);
|
||||||
|
rpc = shapeshift.getRpc();
|
||||||
|
});
|
||||||
|
|
||||||
describe('GET', () => {
|
describe('GET', () => {
|
||||||
const REPLY = { test: 'this is some result' };
|
const REPLY = { test: 'this is some result' };
|
||||||
|
|
||||||
@ -29,7 +38,7 @@ describe('shapeshift/rpc', () => {
|
|||||||
let result;
|
let result;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
scope = mockget([{ path: 'test', reply: REPLY }]);
|
scope = mockget(shapeshift, [{ path: 'test', reply: REPLY }]);
|
||||||
|
|
||||||
return rpc
|
return rpc
|
||||||
.get('test')
|
.get('test')
|
||||||
@ -54,7 +63,7 @@ describe('shapeshift/rpc', () => {
|
|||||||
let result;
|
let result;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
scope = mockpost([{ path: 'test', reply: REPLY }]);
|
scope = mockpost(shapeshift, [{ path: 'test', reply: REPLY }]);
|
||||||
|
|
||||||
return rpc
|
return rpc
|
||||||
.post('test', { input: 'stuff' })
|
.post('test', { input: 'stuff' })
|
||||||
@ -76,7 +85,7 @@ describe('shapeshift/rpc', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('passes the apikey specified', () => {
|
it('passes the apikey specified', () => {
|
||||||
expect(scope.body.test.apiKey).to.equal(APIKEY);
|
expect(scope.body.test.apiKey).to.equal(helpers.APIKEY);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
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/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
export default function (rpc) {
|
export default function (rpc) {
|
||||||
let subscriptions = [];
|
let _subscriptions = [];
|
||||||
let pollStatusIntervalId = null;
|
let _pollStatusIntervalId = null;
|
||||||
|
let _subscriptionPromises = null;
|
||||||
|
|
||||||
function getCoins () {
|
function getCoins () {
|
||||||
return rpc.get('getcoins');
|
return rpc.get('getcoins');
|
||||||
@ -36,75 +37,93 @@ export default function (rpc) {
|
|||||||
|
|
||||||
function shift (toAddress, returnAddress, pair) {
|
function shift (toAddress, returnAddress, pair) {
|
||||||
return rpc.post('shift', {
|
return rpc.post('shift', {
|
||||||
withdrawal: toAddress,
|
pair,
|
||||||
pair: pair,
|
returnAddress,
|
||||||
returnAddress: returnAddress
|
withdrawal: toAddress
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function subscribe (depositAddress, callback) {
|
function subscribe (depositAddress, callback) {
|
||||||
const idx = subscriptions.length;
|
if (!depositAddress || !callback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
subscriptions.push({
|
const index = _subscriptions.length;
|
||||||
depositAddress,
|
|
||||||
|
_subscriptions.push({
|
||||||
callback,
|
callback,
|
||||||
idx
|
depositAddress,
|
||||||
|
index
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only poll if there are subscriptions...
|
if (_pollStatusIntervalId === null) {
|
||||||
if (!pollStatusIntervalId) {
|
_pollStatusIntervalId = setInterval(_pollStatus, 2000);
|
||||||
pollStatusIntervalId = setInterval(_pollStatus, 2000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function unsubscribe (depositAddress) {
|
function unsubscribe (depositAddress) {
|
||||||
const newSubscriptions = []
|
_subscriptions = _subscriptions.filter((sub) => sub.depositAddress !== depositAddress);
|
||||||
.concat(subscriptions)
|
|
||||||
.filter((sub) => sub.depositAddress !== depositAddress);
|
|
||||||
|
|
||||||
subscriptions = newSubscriptions;
|
if (_subscriptions.length === 0) {
|
||||||
|
clearInterval(_pollStatusIntervalId);
|
||||||
if (subscriptions.length === 0) {
|
_pollStatusIntervalId = null;
|
||||||
clearInterval(pollStatusIntervalId);
|
|
||||||
pollStatusIntervalId = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getSubscriptionStatus (subscription) {
|
function _getSubscriptionStatus (subscription) {
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
return;
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus(subscription.depositAddress)
|
return getStatus(subscription.depositAddress)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
switch (result.status) {
|
switch (result.status) {
|
||||||
case 'no_deposits':
|
case 'no_deposits':
|
||||||
case 'received':
|
case 'received':
|
||||||
subscription.callback(null, result);
|
subscription.callback(null, result);
|
||||||
return;
|
return true;
|
||||||
|
|
||||||
case 'complete':
|
case 'complete':
|
||||||
subscription.callback(null, result);
|
subscription.callback(null, result);
|
||||||
subscriptions[subscription.idx] = null;
|
unsubscribe(subscription.depositAddress);
|
||||||
return;
|
return true;
|
||||||
|
|
||||||
case 'failed':
|
case 'failed':
|
||||||
subscription.callback({
|
subscription.callback({
|
||||||
message: status.error,
|
message: status.error,
|
||||||
fatal: true
|
fatal: true
|
||||||
});
|
});
|
||||||
subscriptions[subscription.idx] = null;
|
unsubscribe(subscription.depositAddress);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(subscription.callback);
|
.catch(() => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _pollStatus () {
|
function _pollStatus () {
|
||||||
subscriptions.forEach(_getSubscriptionStatus);
|
_subscriptionPromises = Promise.all(_subscriptions.map(_getSubscriptionStatus));
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getSubscriptions () {
|
||||||
|
return _subscriptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getSubscriptionPromises () {
|
||||||
|
return _subscriptionPromises;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isPolling () {
|
||||||
|
return _pollStatusIntervalId !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
_getSubscriptions,
|
||||||
|
_getSubscriptionPromises,
|
||||||
|
_isPolling,
|
||||||
getCoins,
|
getCoins,
|
||||||
getMarketInfo,
|
getMarketInfo,
|
||||||
getRpc,
|
getRpc,
|
||||||
|
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
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const sinon = require('sinon');
|
||||||
|
|
||||||
|
const ShapeShift = require('./');
|
||||||
|
const initShapeshift = (ShapeShift.default || ShapeShift);
|
||||||
|
|
||||||
const helpers = require('./helpers.spec.js');
|
const helpers = require('./helpers.spec.js');
|
||||||
|
|
||||||
const mockget = helpers.mockget;
|
const mockget = helpers.mockget;
|
||||||
const mockpost = helpers.mockpost;
|
const mockpost = helpers.mockpost;
|
||||||
const shapeshift = helpers.shapeshift;
|
|
||||||
|
|
||||||
describe('shapeshift/calls', () => {
|
describe('shapeshift/calls', () => {
|
||||||
|
let clock;
|
||||||
|
let shapeshift;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
clock = sinon.useFakeTimers();
|
||||||
|
shapeshift = initShapeshift(helpers.APIKEY);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
describe('getCoins', () => {
|
describe('getCoins', () => {
|
||||||
const REPLY = {
|
const REPLY = {
|
||||||
BTC: {
|
BTC: {
|
||||||
@ -39,8 +55,8 @@ describe('shapeshift/calls', () => {
|
|||||||
|
|
||||||
let scope;
|
let scope;
|
||||||
|
|
||||||
before(() => {
|
beforeEach(() => {
|
||||||
scope = mockget([{ path: 'getcoins', reply: REPLY }]);
|
scope = mockget(shapeshift, [{ path: 'getcoins', reply: REPLY }]);
|
||||||
|
|
||||||
return shapeshift.getCoins();
|
return shapeshift.getCoins();
|
||||||
});
|
});
|
||||||
@ -61,8 +77,8 @@ describe('shapeshift/calls', () => {
|
|||||||
|
|
||||||
let scope;
|
let scope;
|
||||||
|
|
||||||
before(() => {
|
beforeEach(() => {
|
||||||
scope = mockget([{ path: 'marketinfo/btc_ltc', reply: REPLY }]);
|
scope = mockget(shapeshift, [{ path: 'marketinfo/btc_ltc', reply: REPLY }]);
|
||||||
|
|
||||||
return shapeshift.getMarketInfo('btc_ltc');
|
return shapeshift.getMarketInfo('btc_ltc');
|
||||||
});
|
});
|
||||||
@ -80,8 +96,8 @@ describe('shapeshift/calls', () => {
|
|||||||
|
|
||||||
let scope;
|
let scope;
|
||||||
|
|
||||||
before(() => {
|
beforeEach(() => {
|
||||||
scope = mockget([{ path: 'txStat/0x123', reply: REPLY }]);
|
scope = mockget(shapeshift, [{ path: 'txStat/0x123', reply: REPLY }]);
|
||||||
|
|
||||||
return shapeshift.getStatus('0x123');
|
return shapeshift.getStatus('0x123');
|
||||||
});
|
});
|
||||||
@ -101,8 +117,8 @@ describe('shapeshift/calls', () => {
|
|||||||
|
|
||||||
let scope;
|
let scope;
|
||||||
|
|
||||||
before(() => {
|
beforeEach(() => {
|
||||||
scope = mockpost([{ path: 'shift', reply: REPLY }]);
|
scope = mockpost(shapeshift, [{ path: 'shift', reply: REPLY }]);
|
||||||
|
|
||||||
return shapeshift.shift('0x456', '1BTC', 'btc_eth');
|
return shapeshift.shift('0x456', '1BTC', 'btc_eth');
|
||||||
});
|
});
|
||||||
@ -125,4 +141,80 @@ describe('shapeshift/calls', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('subscriptions', () => {
|
||||||
|
const ADDRESS = '0123456789abcdef';
|
||||||
|
const REPLY = {
|
||||||
|
status: 'complete',
|
||||||
|
address: ADDRESS
|
||||||
|
};
|
||||||
|
|
||||||
|
let callback;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockget(shapeshift, [{ path: `txStat/${ADDRESS}`, reply: REPLY }]);
|
||||||
|
callback = sinon.stub();
|
||||||
|
shapeshift.subscribe(ADDRESS, callback);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('subscribe', () => {
|
||||||
|
it('adds the depositAddress to the list', () => {
|
||||||
|
const subscriptions = shapeshift._getSubscriptions();
|
||||||
|
|
||||||
|
expect(subscriptions.length).to.equal(1);
|
||||||
|
expect(subscriptions[0].depositAddress).to.equal(ADDRESS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts the polling timer', () => {
|
||||||
|
expect(shapeshift._isPolling()).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the callback once the timer has elapsed', () => {
|
||||||
|
clock.tick(2222);
|
||||||
|
|
||||||
|
return shapeshift._getSubscriptionPromises().then(() => {
|
||||||
|
expect(callback).to.have.been.calledWith(null, REPLY);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('auto-unsubscribes on completed', () => {
|
||||||
|
clock.tick(2222);
|
||||||
|
|
||||||
|
return shapeshift._getSubscriptionPromises().then(() => {
|
||||||
|
expect(shapeshift._getSubscriptions().length).to.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unsubscribe', () => {
|
||||||
|
it('unbsubscribes when requested', () => {
|
||||||
|
expect(shapeshift._getSubscriptions().length).to.equal(1);
|
||||||
|
shapeshift.unsubscribe(ADDRESS);
|
||||||
|
expect(shapeshift._getSubscriptions().length).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears the polling on no subscriptions', () => {
|
||||||
|
shapeshift.unsubscribe(ADDRESS);
|
||||||
|
expect(shapeshift._isPolling()).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles unsubscribe of auto-unsubscribe', () => {
|
||||||
|
clock.tick(2222);
|
||||||
|
|
||||||
|
return shapeshift._getSubscriptionPromises().then(() => {
|
||||||
|
expect(shapeshift.unsubscribe(ADDRESS)).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles unsubscribe when multiples listed', () => {
|
||||||
|
const ADDRESS2 = 'abcdef0123456789';
|
||||||
|
|
||||||
|
shapeshift.subscribe(ADDRESS2, sinon.stub());
|
||||||
|
expect(shapeshift._getSubscriptions().length).to.equal(2);
|
||||||
|
expect(shapeshift._getSubscriptions()[0].depositAddress).to.equal(ADDRESS);
|
||||||
|
shapeshift.unsubscribe(ADDRESS);
|
||||||
|
expect(shapeshift._getSubscriptions()[0].depositAddress).to.equal(ADDRESS2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -31,6 +31,12 @@ export default class Param {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static toParams (params) {
|
static toParams (params) {
|
||||||
return params.map((param) => new Param(param.name, param.type));
|
return params.map((param) => {
|
||||||
|
if (param instanceof Param) {
|
||||||
|
return param;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Param(param.name, param.type);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,5 +34,14 @@ describe('abi/spec/Param', () => {
|
|||||||
expect(params[0].name).to.equal('foo');
|
expect(params[0].name).to.equal('foo');
|
||||||
expect(params[0].kind.type).to.equal('uint');
|
expect(params[0].kind.type).to.equal('uint');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('converts only if needed', () => {
|
||||||
|
const _params = Param.toParams([{ name: 'foo', type: 'uint' }]);
|
||||||
|
const params = Param.toParams(_params);
|
||||||
|
|
||||||
|
expect(params.length).to.equal(1);
|
||||||
|
expect(params[0].name).to.equal('foo');
|
||||||
|
expect(params[0].kind.type).to.equal('uint');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import 'sinon-as-promised';
|
|
||||||
|
|
||||||
import Eth from './eth';
|
import Eth from './eth';
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import 'sinon-as-promised';
|
|
||||||
|
|
||||||
import Personal from './personal';
|
import Personal from './personal';
|
||||||
|
|
||||||
|
@ -26,7 +26,9 @@ export function decodeCallData (data) {
|
|||||||
|
|
||||||
if (data.substr(0, 2) === '0x') {
|
if (data.substr(0, 2) === '0x') {
|
||||||
return decodeCallData(data.slice(2));
|
return decodeCallData(data.slice(2));
|
||||||
} else if (data.length < 8) {
|
}
|
||||||
|
|
||||||
|
if (data.length < 8) {
|
||||||
throw new Error('Input to decodeCallData should be method signature + data');
|
throw new Error('Input to decodeCallData should be method signature + data');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,10 +44,14 @@ export function decodeCallData (data) {
|
|||||||
export function decodeMethodInput (methodAbi, paramdata) {
|
export function decodeMethodInput (methodAbi, paramdata) {
|
||||||
if (!methodAbi) {
|
if (!methodAbi) {
|
||||||
throw new Error('decodeMethodInput should receive valid method-specific ABI');
|
throw new Error('decodeMethodInput should receive valid method-specific ABI');
|
||||||
} else if (paramdata && paramdata.length) {
|
}
|
||||||
|
|
||||||
|
if (paramdata && paramdata.length) {
|
||||||
if (!isHex(paramdata)) {
|
if (!isHex(paramdata)) {
|
||||||
throw new Error('Input to decodeMethodInput should be a hex value');
|
throw new Error('Input to decodeMethodInput should be a hex value');
|
||||||
} else if (paramdata.substr(0, 2) === '0x') {
|
}
|
||||||
|
|
||||||
|
if (paramdata.substr(0, 2) === '0x') {
|
||||||
return decodeMethodInput(methodAbi, paramdata.slice(2));
|
return decodeMethodInput(methodAbi, paramdata.slice(2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ const TEST_ENV = process.env.NODE_ENV === 'test';
|
|||||||
|
|
||||||
export function createIdentityImg (address, scale = 8) {
|
export function createIdentityImg (address, scale = 8) {
|
||||||
return TEST_ENV
|
return TEST_ENV
|
||||||
? ''
|
? 'test-createIdentityImg'
|
||||||
: blockies({
|
: blockies({
|
||||||
seed: (address || '').toLowerCase(),
|
seed: (address || '').toLowerCase(),
|
||||||
size: 8,
|
size: 8,
|
||||||
|
@ -14,8 +14,21 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import { keccak_256 } from 'js-sha3'; // eslint-disable-line camelcase
|
import CryptoJS from 'crypto-js';
|
||||||
|
import CryptoSha3 from 'crypto-js/sha3';
|
||||||
|
|
||||||
export function sha3 (value) {
|
export function sha3 (value, options) {
|
||||||
return `0x${keccak_256(value)}`;
|
if (options && options.encoding === 'hex') {
|
||||||
|
if (value.length > 2 && value.substr(0, 2) === '0x') {
|
||||||
|
value = value.substr(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
value = CryptoJS.enc.Hex.parse(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = CryptoSha3(value, {
|
||||||
|
outputLength: 256
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
return `0x${hash}`;
|
||||||
}
|
}
|
||||||
|
@ -34,3 +34,16 @@ ReactDOM.render(
|
|||||||
</Provider>,
|
</Provider>,
|
||||||
document.querySelector('#container')
|
document.querySelector('#container')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (module.hot) {
|
||||||
|
module.hot.accept('./registry/Container', () => {
|
||||||
|
require('./registry/Container');
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<Provider store={ store }>
|
||||||
|
<Container />
|
||||||
|
</Provider>,
|
||||||
|
document.querySelector('#container')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -44,8 +44,8 @@ export default class Application extends Component {
|
|||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
accounts: PropTypes.object.isRequired,
|
accounts: PropTypes.object.isRequired,
|
||||||
contract: nullableProptype(PropTypes.object).isRequired,
|
contract: nullableProptype(PropTypes.object.isRequired),
|
||||||
fee: nullableProptype(PropTypes.object).isRequired
|
fee: nullableProptype(PropTypes.object.isRequired)
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -15,11 +15,13 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import { sha3 } from '../parity.js';
|
import { sha3 } from '../parity.js';
|
||||||
|
import { getOwner } from '../util/registry';
|
||||||
|
|
||||||
export const clear = () => ({ type: 'lookup clear' });
|
export const clear = () => ({ type: 'lookup clear' });
|
||||||
|
|
||||||
export const lookupStart = (name, key) => ({ type: 'lookup start', name, key });
|
export const lookupStart = (name, key) => ({ type: 'lookup start', name, key });
|
||||||
export const reverseLookupStart = (address) => ({ type: 'reverseLookup start', address });
|
export const reverseLookupStart = (address) => ({ type: 'reverseLookup start', address });
|
||||||
|
export const ownerLookupStart = (name) => ({ type: 'ownerLookup start', name });
|
||||||
|
|
||||||
export const success = (action, result) => ({ type: `${action} success`, result: result });
|
export const success = (action, result) => ({ type: `${action} success`, result: result });
|
||||||
|
|
||||||
@ -48,24 +50,50 @@ export const lookup = (name, key) => (dispatch, getState) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reverseLookup = (address) => (dispatch, getState) => {
|
export const reverseLookup = (lookupAddress) => (dispatch, getState) => {
|
||||||
const { contract } = getState();
|
const { contract } = getState();
|
||||||
|
|
||||||
if (!contract) {
|
if (!contract) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reverse = contract.functions
|
dispatch(reverseLookupStart(lookupAddress));
|
||||||
.find((f) => f.name === 'reverse');
|
|
||||||
|
|
||||||
dispatch(reverseLookupStart(address));
|
contract.instance
|
||||||
|
.reverse
|
||||||
reverse.call({}, [ address ])
|
.call({}, [ lookupAddress ])
|
||||||
.then((address) => dispatch(success('reverseLookup', address)))
|
.then((address) => {
|
||||||
|
dispatch(success('reverseLookup', address));
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(`could not lookup reverse for ${address}`);
|
console.error(`could not lookup reverse for ${lookupAddress}`);
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
}
|
}
|
||||||
dispatch(fail('reverseLookup'));
|
dispatch(fail('reverseLookup'));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ownerLookup = (name) => (dispatch, getState) => {
|
||||||
|
const { contract } = getState();
|
||||||
|
|
||||||
|
if (!contract) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(ownerLookupStart(name));
|
||||||
|
|
||||||
|
return getOwner(contract, name)
|
||||||
|
.then((owner) => {
|
||||||
|
dispatch(success('ownerLookup', owner));
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`could not lookup owner for ${name}`);
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
console.error(err.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fail('ownerLookup'));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -23,13 +23,14 @@ import DropDownMenu from 'material-ui/DropDownMenu';
|
|||||||
import MenuItem from 'material-ui/MenuItem';
|
import MenuItem from 'material-ui/MenuItem';
|
||||||
import RaisedButton from 'material-ui/RaisedButton';
|
import RaisedButton from 'material-ui/RaisedButton';
|
||||||
import SearchIcon from 'material-ui/svg-icons/action/search';
|
import SearchIcon from 'material-ui/svg-icons/action/search';
|
||||||
|
import keycode from 'keycode';
|
||||||
|
|
||||||
import { nullableProptype } from '~/util/proptypes';
|
import { nullableProptype } from '~/util/proptypes';
|
||||||
|
|
||||||
import Address from '../ui/address.js';
|
import Address from '../ui/address.js';
|
||||||
import renderImage from '../ui/image.js';
|
import renderImage from '../ui/image.js';
|
||||||
|
|
||||||
import { clear, lookup, reverseLookup } from './actions';
|
import { clear, lookup, ownerLookup, reverseLookup } from './actions';
|
||||||
import styles from './lookup.css';
|
import styles from './lookup.css';
|
||||||
|
|
||||||
class Lookup extends Component {
|
class Lookup extends Component {
|
||||||
@ -39,6 +40,7 @@ class Lookup extends Component {
|
|||||||
|
|
||||||
clear: PropTypes.func.isRequired,
|
clear: PropTypes.func.isRequired,
|
||||||
lookup: PropTypes.func.isRequired,
|
lookup: PropTypes.func.isRequired,
|
||||||
|
ownerLookup: PropTypes.func.isRequired,
|
||||||
reverseLookup: PropTypes.func.isRequired
|
reverseLookup: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,33 +52,6 @@ class Lookup extends Component {
|
|||||||
const { input, type } = this.state;
|
const { input, type } = this.state;
|
||||||
const { result } = this.props;
|
const { result } = this.props;
|
||||||
|
|
||||||
let output = '';
|
|
||||||
if (result) {
|
|
||||||
if (type === 'A') {
|
|
||||||
output = (
|
|
||||||
<code>
|
|
||||||
<Address
|
|
||||||
address={ result }
|
|
||||||
shortenHash={ false }
|
|
||||||
/>
|
|
||||||
</code>
|
|
||||||
);
|
|
||||||
} else if (type === 'IMG') {
|
|
||||||
output = renderImage(result);
|
|
||||||
} else if (type === 'CONTENT') {
|
|
||||||
output = (
|
|
||||||
<div>
|
|
||||||
<code>{ result }</code>
|
|
||||||
<p>Keep in mind that this is most likely the hash of the content you are looking for.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
output = (
|
|
||||||
<code>{ result }</code>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={ styles.lookup }>
|
<Card className={ styles.lookup }>
|
||||||
<CardHeader title={ 'Query the Registry' } />
|
<CardHeader title={ 'Query the Registry' } />
|
||||||
@ -85,6 +60,7 @@ class Lookup extends Component {
|
|||||||
hintText={ type === 'reverse' ? 'address' : 'name' }
|
hintText={ type === 'reverse' ? 'address' : 'name' }
|
||||||
value={ input }
|
value={ input }
|
||||||
onChange={ this.onInputChange }
|
onChange={ this.onInputChange }
|
||||||
|
onKeyDown={ this.onKeyDown }
|
||||||
/>
|
/>
|
||||||
<DropDownMenu
|
<DropDownMenu
|
||||||
value={ type }
|
value={ type }
|
||||||
@ -94,6 +70,7 @@ class Lookup extends Component {
|
|||||||
<MenuItem value='IMG' primaryText='IMG – hash of a picture in the blockchain' />
|
<MenuItem value='IMG' primaryText='IMG – hash of a picture in the blockchain' />
|
||||||
<MenuItem value='CONTENT' primaryText='CONTENT – hash of a data in the blockchain' />
|
<MenuItem value='CONTENT' primaryText='CONTENT – hash of a data in the blockchain' />
|
||||||
<MenuItem value='reverse' primaryText='reverse – find a name for an address' />
|
<MenuItem value='reverse' primaryText='reverse – find a name for an address' />
|
||||||
|
<MenuItem value='owner' primaryText='owner – find a the owner' />
|
||||||
</DropDownMenu>
|
</DropDownMenu>
|
||||||
<RaisedButton
|
<RaisedButton
|
||||||
label='Lookup'
|
label='Lookup'
|
||||||
@ -102,35 +79,102 @@ class Lookup extends Component {
|
|||||||
onTouchTap={ this.onLookupClick }
|
onTouchTap={ this.onLookupClick }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CardText>{ output }</CardText>
|
<CardText>
|
||||||
|
{ this.renderOutput(type, result) }
|
||||||
|
</CardText>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderOutput (type, result) {
|
||||||
|
if (result === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'A') {
|
||||||
|
return (
|
||||||
|
<code>
|
||||||
|
<Address
|
||||||
|
address={ result }
|
||||||
|
shortenHash={ false }
|
||||||
|
/>
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'owner') {
|
||||||
|
if (!result) {
|
||||||
|
return (
|
||||||
|
<code>Not reserved yet</code>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<code>
|
||||||
|
<Address
|
||||||
|
address={ result }
|
||||||
|
shortenHash={ false }
|
||||||
|
/>
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'IMG') {
|
||||||
|
return renderImage(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'CONTENT') {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<code>{ result }</code>
|
||||||
|
<p>Keep in mind that this is most likely the hash of the content you are looking for.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<code>{ result || 'No data' }</code>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onInputChange = (e) => {
|
onInputChange = (e) => {
|
||||||
this.setState({ input: e.target.value });
|
this.setState({ input: e.target.value });
|
||||||
};
|
}
|
||||||
|
|
||||||
|
onKeyDown = (event) => {
|
||||||
|
const codeName = keycode(event);
|
||||||
|
|
||||||
|
if (codeName !== 'enter') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onLookupClick();
|
||||||
|
}
|
||||||
|
|
||||||
onTypeChange = (e, i, type) => {
|
onTypeChange = (e, i, type) => {
|
||||||
this.setState({ type });
|
this.setState({ type });
|
||||||
this.props.clear();
|
this.props.clear();
|
||||||
};
|
}
|
||||||
|
|
||||||
onLookupClick = () => {
|
onLookupClick = () => {
|
||||||
const { input, type } = this.state;
|
const { input, type } = this.state;
|
||||||
|
|
||||||
if (type === 'reverse') {
|
if (type === 'reverse') {
|
||||||
this.props.reverseLookup(input);
|
return this.props.reverseLookup(input);
|
||||||
} else {
|
}
|
||||||
this.props.lookup(input, type);
|
|
||||||
|
if (type === 'owner') {
|
||||||
|
return this.props.ownerLookup(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.lookup(input, type);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state) => state.lookup;
|
const mapStateToProps = (state) => state.lookup;
|
||||||
const mapDispatchToProps = (dispatch) =>
|
const mapDispatchToProps = (dispatch) =>
|
||||||
bindActionCreators({
|
bindActionCreators({
|
||||||
clear, lookup, reverseLookup
|
clear, lookup, ownerLookup, reverseLookup
|
||||||
}, dispatch);
|
}, dispatch);
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Lookup);
|
export default connect(mapStateToProps, mapDispatchToProps)(Lookup);
|
||||||
|
@ -24,7 +24,7 @@ const initialState = {
|
|||||||
export default (state = initialState, action) => {
|
export default (state = initialState, action) => {
|
||||||
const { type } = action;
|
const { type } = action;
|
||||||
|
|
||||||
if (type.slice(0, 7) !== 'lookup ' && type.slice(0, 14) !== 'reverseLookup ') {
|
if (!/^(lookup|reverseLookup|ownerLookup)/.test(type)) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,8 +15,13 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import { sha3, api } from '../parity.js';
|
import { sha3, api } from '../parity.js';
|
||||||
|
import { getOwner, isOwned } from '../util/registry';
|
||||||
import postTx from '../util/post-tx';
|
import postTx from '../util/post-tx';
|
||||||
|
|
||||||
|
export const clearError = () => ({
|
||||||
|
type: 'clearError'
|
||||||
|
});
|
||||||
|
|
||||||
const alreadyQueued = (queue, action, name) =>
|
const alreadyQueued = (queue, action, name) =>
|
||||||
!!queue.find((entry) => entry.action === action && entry.name === name);
|
!!queue.find((entry) => entry.action === action && entry.name === name);
|
||||||
|
|
||||||
@ -24,13 +29,14 @@ export const reserveStart = (name) => ({ type: 'names reserve start', name });
|
|||||||
|
|
||||||
export const reserveSuccess = (name) => ({ type: 'names reserve success', name });
|
export const reserveSuccess = (name) => ({ type: 'names reserve success', name });
|
||||||
|
|
||||||
export const reserveFail = (name) => ({ type: 'names reserve fail', name });
|
export const reserveFail = (name, error) => ({ type: 'names reserve fail', name, error });
|
||||||
|
|
||||||
export const reserve = (name) => (dispatch, getState) => {
|
export const reserve = (name) => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const account = state.accounts.selected;
|
const account = state.accounts.selected;
|
||||||
const contract = state.contract;
|
const contract = state.contract;
|
||||||
const fee = state.fee;
|
const fee = state.fee;
|
||||||
|
|
||||||
if (!contract || !account) {
|
if (!contract || !account) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -40,10 +46,17 @@ export const reserve = (name) => (dispatch, getState) => {
|
|||||||
if (alreadyQueued(state.names.queue, 'reserve', name)) {
|
if (alreadyQueued(state.names.queue, 'reserve', name)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const reserve = contract.functions.find((f) => f.name === 'reserve');
|
|
||||||
|
|
||||||
dispatch(reserveStart(name));
|
dispatch(reserveStart(name));
|
||||||
|
|
||||||
|
return isOwned(contract, name)
|
||||||
|
.then((owned) => {
|
||||||
|
if (owned) {
|
||||||
|
throw new Error(`"${name}" has already been reserved`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { reserve } = contract.instance;
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
from: account.address,
|
from: account.address,
|
||||||
value: fee
|
value: fee
|
||||||
@ -52,15 +65,15 @@ export const reserve = (name) => (dispatch, getState) => {
|
|||||||
sha3(name)
|
sha3(name)
|
||||||
];
|
];
|
||||||
|
|
||||||
postTx(api, reserve, options, values)
|
return postTx(api, reserve, options, values);
|
||||||
|
})
|
||||||
.then((txHash) => {
|
.then((txHash) => {
|
||||||
dispatch(reserveSuccess(name));
|
dispatch(reserveSuccess(name));
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(`could not reserve ${name}`);
|
if (err.type !== 'REQUEST_REJECTED') {
|
||||||
|
console.error(`error rerserving ${name}`, err);
|
||||||
if (err) {
|
return dispatch(reserveFail(name, err));
|
||||||
console.error(err.stack);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(reserveFail(name));
|
dispatch(reserveFail(name));
|
||||||
@ -71,43 +84,52 @@ export const dropStart = (name) => ({ type: 'names drop start', name });
|
|||||||
|
|
||||||
export const dropSuccess = (name) => ({ type: 'names drop success', name });
|
export const dropSuccess = (name) => ({ type: 'names drop success', name });
|
||||||
|
|
||||||
export const dropFail = (name) => ({ type: 'names drop fail', name });
|
export const dropFail = (name, error) => ({ type: 'names drop fail', name, error });
|
||||||
|
|
||||||
export const drop = (name) => (dispatch, getState) => {
|
export const drop = (name) => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const account = state.accounts.selected;
|
const account = state.accounts.selected;
|
||||||
const contract = state.contract;
|
const contract = state.contract;
|
||||||
|
|
||||||
if (!contract || !account) {
|
if (!contract || !account) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
name = name.toLowerCase();
|
name = name.toLowerCase();
|
||||||
|
|
||||||
if (alreadyQueued(state.names.queue, 'drop', name)) {
|
if (alreadyQueued(state.names.queue, 'drop', name)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const drop = contract.functions.find((f) => f.name === 'drop');
|
|
||||||
|
|
||||||
dispatch(dropStart(name));
|
dispatch(dropStart(name));
|
||||||
|
|
||||||
|
return getOwner(contract, name)
|
||||||
|
.then((owner) => {
|
||||||
|
if (owner.toLowerCase() !== account.address.toLowerCase()) {
|
||||||
|
throw new Error(`you are not the owner of "${name}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { drop } = contract.instance;
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
from: account.address
|
from: account.address
|
||||||
};
|
};
|
||||||
|
|
||||||
const values = [
|
const values = [
|
||||||
sha3(name)
|
sha3(name)
|
||||||
];
|
];
|
||||||
|
|
||||||
postTx(api, drop, options, values)
|
return postTx(api, drop, options, values);
|
||||||
|
})
|
||||||
.then((txhash) => {
|
.then((txhash) => {
|
||||||
dispatch(dropSuccess(name));
|
dispatch(dropSuccess(name));
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(`could not drop ${name}`);
|
if (err.type !== 'REQUEST_REJECTED') {
|
||||||
|
console.error(`error dropping ${name}`, err);
|
||||||
if (err) {
|
return dispatch(dropFail(name, err));
|
||||||
console.error(err.stack);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(reserveFail(name));
|
dispatch(dropFail(name));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -35,7 +35,12 @@
|
|||||||
.link {
|
.link {
|
||||||
color: #00BCD4;
|
color: #00BCD4;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
|
||||||
.link:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,10 @@ import MenuItem from 'material-ui/MenuItem';
|
|||||||
import RaisedButton from 'material-ui/RaisedButton';
|
import RaisedButton from 'material-ui/RaisedButton';
|
||||||
import CheckIcon from 'material-ui/svg-icons/navigation/check';
|
import CheckIcon from 'material-ui/svg-icons/navigation/check';
|
||||||
|
|
||||||
|
import { nullableProptype } from '~/util/proptypes';
|
||||||
import { fromWei } from '../parity.js';
|
import { fromWei } from '../parity.js';
|
||||||
|
|
||||||
import { reserve, drop } from './actions';
|
import { clearError, reserve, drop } from './actions';
|
||||||
import styles from './names.css';
|
import styles from './names.css';
|
||||||
|
|
||||||
const useSignerText = (<p>Use the <a href='/#/signer' className={ styles.link } target='_blank'>Signer</a> to authenticate the following changes.</p>);
|
const useSignerText = (<p>Use the <a href='/#/signer' className={ styles.link } target='_blank'>Signer</a> to authenticate the following changes.</p>);
|
||||||
@ -78,35 +79,21 @@ const renderQueue = (queue) => {
|
|||||||
class Names extends Component {
|
class Names extends Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
error: nullableProptype(PropTypes.object.isRequired),
|
||||||
fee: PropTypes.object.isRequired,
|
fee: PropTypes.object.isRequired,
|
||||||
pending: PropTypes.bool.isRequired,
|
pending: PropTypes.bool.isRequired,
|
||||||
queue: PropTypes.array.isRequired,
|
queue: PropTypes.array.isRequired,
|
||||||
|
|
||||||
|
clearError: PropTypes.func.isRequired,
|
||||||
reserve: PropTypes.func.isRequired,
|
reserve: PropTypes.func.isRequired,
|
||||||
drop: PropTypes.func.isRequired
|
drop: PropTypes.func.isRequired
|
||||||
}
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
action: 'reserve',
|
action: 'reserve',
|
||||||
name: ''
|
name: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
|
||||||
const nextQueue = nextProps.queue;
|
|
||||||
const prevQueue = this.props.queue;
|
|
||||||
|
|
||||||
if (nextQueue.length > prevQueue.length) {
|
|
||||||
const newQueued = nextQueue[nextQueue.length - 1];
|
|
||||||
const newName = newQueued.name;
|
|
||||||
|
|
||||||
if (newName !== this.state.name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ name: '' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { action, name } = this.state;
|
const { action, name } = this.state;
|
||||||
const { fee, pending, queue } = this.props;
|
const { fee, pending, queue } = this.props;
|
||||||
@ -122,6 +109,7 @@ class Names extends Component {
|
|||||||
: (<p className={ styles.noSpacing }>To drop a name, you have to be the owner.</p>)
|
: (<p className={ styles.noSpacing }>To drop a name, you have to be the owner.</p>)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{ this.renderError() }
|
||||||
<div className={ styles.box }>
|
<div className={ styles.box }>
|
||||||
<TextField
|
<TextField
|
||||||
hintText='name'
|
hintText='name'
|
||||||
@ -154,23 +142,50 @@ class Names extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderError () {
|
||||||
|
const { error } = this.props;
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={ styles.error }>
|
||||||
|
<code>{ error.message }</code>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onNameChange = (e) => {
|
onNameChange = (e) => {
|
||||||
|
this.clearError();
|
||||||
this.setState({ name: e.target.value });
|
this.setState({ name: e.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onActionChange = (e, i, action) => {
|
onActionChange = (e, i, action) => {
|
||||||
|
this.clearError();
|
||||||
this.setState({ action });
|
this.setState({ action });
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmitClick = () => {
|
onSubmitClick = () => {
|
||||||
const { action, name } = this.state;
|
const { action, name } = this.state;
|
||||||
|
|
||||||
if (action === 'reserve') {
|
if (action === 'reserve') {
|
||||||
this.props.reserve(name);
|
return this.props.reserve(name);
|
||||||
} else if (action === 'drop') {
|
}
|
||||||
this.props.drop(name);
|
|
||||||
|
if (action === 'drop') {
|
||||||
|
return this.props.drop(name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
clearError = () => {
|
||||||
|
if (this.props.error) {
|
||||||
|
this.props.clearError();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({ ...state.names, fee: state.fee });
|
const mapStateToProps = (state) => ({ ...state.names, fee: state.fee });
|
||||||
const mapDispatchToProps = (dispatch) => bindActionCreators({ reserve, drop }, dispatch);
|
const mapDispatchToProps = (dispatch) => bindActionCreators({ clearError, reserve, drop }, dispatch);
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Names);
|
export default connect(mapStateToProps, mapDispatchToProps)(Names);
|
||||||
|
@ -17,32 +17,55 @@
|
|||||||
import { isAction, isStage, addToQueue, removeFromQueue } from '../util/actions';
|
import { isAction, isStage, addToQueue, removeFromQueue } from '../util/actions';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
error: null,
|
||||||
pending: false,
|
pending: false,
|
||||||
queue: []
|
queue: []
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (state = initialState, action) => {
|
export default (state = initialState, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'clearError':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (isAction('names', 'reserve', action)) {
|
if (isAction('names', 'reserve', action)) {
|
||||||
if (isStage('start', action)) {
|
if (isStage('start', action)) {
|
||||||
return {
|
return {
|
||||||
...state, pending: true,
|
...state,
|
||||||
|
error: null,
|
||||||
|
pending: true,
|
||||||
queue: addToQueue(state.queue, 'reserve', action.name)
|
queue: addToQueue(state.queue, 'reserve', action.name)
|
||||||
};
|
};
|
||||||
} else if (isStage('success', action) || isStage('fail', action)) {
|
}
|
||||||
|
|
||||||
|
if (isStage('success', action) || isStage('fail', action)) {
|
||||||
return {
|
return {
|
||||||
...state, pending: false,
|
...state,
|
||||||
|
error: action.error || null,
|
||||||
|
pending: false,
|
||||||
queue: removeFromQueue(state.queue, 'reserve', action.name)
|
queue: removeFromQueue(state.queue, 'reserve', action.name)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (isAction('names', 'drop', action)) {
|
}
|
||||||
|
|
||||||
|
if (isAction('names', 'drop', action)) {
|
||||||
if (isStage('start', action)) {
|
if (isStage('start', action)) {
|
||||||
return {
|
return {
|
||||||
...state, pending: true,
|
...state,
|
||||||
|
error: null,
|
||||||
|
pending: true,
|
||||||
queue: addToQueue(state.queue, 'drop', action.name)
|
queue: addToQueue(state.queue, 'drop', action.name)
|
||||||
};
|
};
|
||||||
} else if (isStage('success', action) || isStage('fail', action)) {
|
}
|
||||||
|
|
||||||
|
if (isStage('success', action) || isStage('fail', action)) {
|
||||||
return {
|
return {
|
||||||
...state, pending: false,
|
...state,
|
||||||
|
error: action.error || null,
|
||||||
|
pending: false,
|
||||||
queue: removeFromQueue(state.queue, 'drop', action.name)
|
queue: removeFromQueue(state.queue, 'drop', action.name)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -16,45 +16,57 @@
|
|||||||
|
|
||||||
import { sha3, api } from '../parity.js';
|
import { sha3, api } from '../parity.js';
|
||||||
import postTx from '../util/post-tx';
|
import postTx from '../util/post-tx';
|
||||||
|
import { getOwner } from '../util/registry';
|
||||||
|
|
||||||
|
export const clearError = () => ({
|
||||||
|
type: 'clearError'
|
||||||
|
});
|
||||||
|
|
||||||
export const start = (name, key, value) => ({ type: 'records update start', name, key, value });
|
export const start = (name, key, value) => ({ type: 'records update start', name, key, value });
|
||||||
|
|
||||||
export const success = () => ({ type: 'records update success' });
|
export const success = () => ({ type: 'records update success' });
|
||||||
|
|
||||||
export const fail = () => ({ type: 'records update error' });
|
export const fail = (error) => ({ type: 'records update fail', error });
|
||||||
|
|
||||||
export const update = (name, key, value) => (dispatch, getState) => {
|
export const update = (name, key, value) => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const account = state.accounts.selected;
|
const account = state.accounts.selected;
|
||||||
const contract = state.contract;
|
const contract = state.contract;
|
||||||
|
|
||||||
if (!contract || !account) {
|
if (!contract || !account) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
name = name.toLowerCase();
|
name = name.toLowerCase();
|
||||||
|
dispatch(start(name, key, value));
|
||||||
|
|
||||||
|
return getOwner(contract, name)
|
||||||
|
.then((owner) => {
|
||||||
|
if (owner.toLowerCase() !== account.address.toLowerCase()) {
|
||||||
|
throw new Error(`you are not the owner of "${name}"`);
|
||||||
|
}
|
||||||
|
|
||||||
const fnName = key === 'A' ? 'setAddress' : 'set';
|
const fnName = key === 'A' ? 'setAddress' : 'set';
|
||||||
const setAddress = contract.functions.find((f) => f.name === fnName);
|
const method = contract.instance[fnName];
|
||||||
|
|
||||||
dispatch(start(name, key, value));
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
from: account.address
|
from: account.address
|
||||||
};
|
};
|
||||||
|
|
||||||
const values = [
|
const values = [
|
||||||
sha3(name),
|
sha3(name),
|
||||||
key,
|
key,
|
||||||
value
|
value
|
||||||
];
|
];
|
||||||
|
|
||||||
postTx(api, setAddress, options, values)
|
return postTx(api, method, options, values);
|
||||||
|
})
|
||||||
.then((txHash) => {
|
.then((txHash) => {
|
||||||
dispatch(success());
|
dispatch(success());
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error(`could not update ${key} record of ${name}`);
|
if (err.type !== 'REQUEST_REJECTED') {
|
||||||
|
console.error(`error updating ${name}`, err);
|
||||||
if (err) {
|
return dispatch(fail(err));
|
||||||
console.error(err.stack);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(fail());
|
dispatch(fail());
|
||||||
|
@ -36,3 +36,7 @@
|
|||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
@ -24,17 +24,20 @@ import MenuItem from 'material-ui/MenuItem';
|
|||||||
import RaisedButton from 'material-ui/RaisedButton';
|
import RaisedButton from 'material-ui/RaisedButton';
|
||||||
import SaveIcon from 'material-ui/svg-icons/content/save';
|
import SaveIcon from 'material-ui/svg-icons/content/save';
|
||||||
|
|
||||||
import { update } from './actions';
|
import { nullableProptype } from '~/util/proptypes';
|
||||||
|
import { clearError, update } from './actions';
|
||||||
import styles from './records.css';
|
import styles from './records.css';
|
||||||
|
|
||||||
class Records extends Component {
|
class Records extends Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
error: nullableProptype(PropTypes.object.isRequired),
|
||||||
pending: PropTypes.bool.isRequired,
|
pending: PropTypes.bool.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
clearError: PropTypes.func.isRequired,
|
||||||
update: PropTypes.func.isRequired
|
update: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,6 +56,7 @@ class Records extends Component {
|
|||||||
<p className={ styles.noSpacing }>
|
<p className={ styles.noSpacing }>
|
||||||
You can only modify entries of names that you previously registered.
|
You can only modify entries of names that you previously registered.
|
||||||
</p>
|
</p>
|
||||||
|
{ this.renderError() }
|
||||||
<div className={ styles.box }>
|
<div className={ styles.box }>
|
||||||
<TextField
|
<TextField
|
||||||
hintText='name'
|
hintText='name'
|
||||||
@ -88,22 +92,46 @@ class Records extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderError () {
|
||||||
|
const { error } = this.props;
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={ styles.error }>
|
||||||
|
<code>{ error.message }</code>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onNameChange = (e) => {
|
onNameChange = (e) => {
|
||||||
|
this.clearError();
|
||||||
this.setState({ name: e.target.value });
|
this.setState({ name: e.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onTypeChange = (e, i, type) => {
|
onTypeChange = (e, i, type) => {
|
||||||
this.setState({ type });
|
this.setState({ type });
|
||||||
};
|
};
|
||||||
|
|
||||||
onValueChange = (e) => {
|
onValueChange = (e) => {
|
||||||
this.setState({ value: e.target.value });
|
this.setState({ value: e.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onSaveClick = () => {
|
onSaveClick = () => {
|
||||||
const { name, type, value } = this.state;
|
const { name, type, value } = this.state;
|
||||||
this.props.update(name, type, value);
|
this.props.update(name, type, value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
clearError = () => {
|
||||||
|
if (this.props.error) {
|
||||||
|
this.props.clearError();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state) => state.records;
|
const mapStateToProps = (state) => state.records;
|
||||||
const mapDispatchToProps = (dispatch) => bindActionCreators({ update }, dispatch);
|
const mapDispatchToProps = (dispatch) => bindActionCreators({ clearError, update }, dispatch);
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Records);
|
export default connect(mapStateToProps, mapDispatchToProps)(Records);
|
||||||
|
@ -17,11 +17,20 @@
|
|||||||
import { isAction, isStage } from '../util/actions';
|
import { isAction, isStage } from '../util/actions';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
error: null,
|
||||||
pending: false,
|
pending: false,
|
||||||
name: '', type: '', value: ''
|
name: '', type: '', value: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (state = initialState, action) => {
|
export default (state = initialState, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'clearError':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAction('records', 'update', action)) {
|
if (!isAction('records', 'update', action)) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -29,11 +38,15 @@ export default (state = initialState, action) => {
|
|||||||
if (isStage('start', action)) {
|
if (isStage('start', action)) {
|
||||||
return {
|
return {
|
||||||
...state, pending: true,
|
...state, pending: true,
|
||||||
name: action.name, type: action.entry, value: action.value
|
error: null,
|
||||||
|
name: action.name, type: action.key, value: action.value
|
||||||
};
|
};
|
||||||
} else if (isStage('success', action) || isStage('fail', action)) {
|
}
|
||||||
|
|
||||||
|
if (isStage('success', action) || isStage('fail', action)) {
|
||||||
return {
|
return {
|
||||||
...state, pending: false,
|
...state, pending: false,
|
||||||
|
error: action.error || null,
|
||||||
name: initialState.name, type: initialState.type, value: initialState.value
|
name: initialState.name, type: initialState.type, value: initialState.value
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -16,44 +16,58 @@
|
|||||||
|
|
||||||
import { api } from '../parity.js';
|
import { api } from '../parity.js';
|
||||||
import postTx from '../util/post-tx';
|
import postTx from '../util/post-tx';
|
||||||
|
import { getOwner } from '../util/registry';
|
||||||
|
|
||||||
|
export const clearError = () => ({
|
||||||
|
type: 'clearError'
|
||||||
|
});
|
||||||
|
|
||||||
export const start = (action, name, address) => ({ type: `reverse ${action} start`, name, address });
|
export const start = (action, name, address) => ({ type: `reverse ${action} start`, name, address });
|
||||||
|
|
||||||
export const success = (action) => ({ type: `reverse ${action} success` });
|
export const success = (action) => ({ type: `reverse ${action} success` });
|
||||||
|
|
||||||
export const fail = (action) => ({ type: `reverse ${action} error` });
|
export const fail = (action, error) => ({ type: `reverse ${action} fail`, error });
|
||||||
|
|
||||||
export const propose = (name, address) => (dispatch, getState) => {
|
export const propose = (name, address) => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const account = state.accounts.selected;
|
const account = state.accounts.selected;
|
||||||
const contract = state.contract;
|
const contract = state.contract;
|
||||||
|
|
||||||
if (!contract || !account) {
|
if (!contract || !account) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
name = name.toLowerCase();
|
name = name.toLowerCase();
|
||||||
|
|
||||||
const proposeReverse = contract.functions.find((f) => f.name === 'proposeReverse');
|
|
||||||
|
|
||||||
dispatch(start('propose', name, address));
|
dispatch(start('propose', name, address));
|
||||||
|
|
||||||
|
return getOwner(contract, name)
|
||||||
|
.then((owner) => {
|
||||||
|
if (owner.toLowerCase() !== account.address.toLowerCase()) {
|
||||||
|
throw new Error(`you are not the owner of "${name}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { proposeReverse } = contract.instance;
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
from: account.address
|
from: account.address
|
||||||
};
|
};
|
||||||
|
|
||||||
const values = [
|
const values = [
|
||||||
name,
|
name,
|
||||||
address
|
address
|
||||||
];
|
];
|
||||||
|
|
||||||
postTx(api, proposeReverse, options, values)
|
return postTx(api, proposeReverse, options, values);
|
||||||
|
})
|
||||||
.then((txHash) => {
|
.then((txHash) => {
|
||||||
dispatch(success('propose'));
|
dispatch(success('propose'));
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(`could not propose reverse ${name} for address ${address}`);
|
if (err.type !== 'REQUEST_REJECTED') {
|
||||||
if (err) {
|
console.error(`error proposing ${name}`, err);
|
||||||
console.error(err.stack);
|
return dispatch(fail('propose', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(fail('propose'));
|
dispatch(fail('propose'));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -62,31 +76,42 @@ export const confirm = (name) => (dispatch, getState) => {
|
|||||||
const state = getState();
|
const state = getState();
|
||||||
const account = state.accounts.selected;
|
const account = state.accounts.selected;
|
||||||
const contract = state.contract;
|
const contract = state.contract;
|
||||||
|
|
||||||
if (!contract || !account) {
|
if (!contract || !account) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
name = name.toLowerCase();
|
name = name.toLowerCase();
|
||||||
|
|
||||||
const confirmReverse = contract.functions.find((f) => f.name === 'confirmReverse');
|
|
||||||
|
|
||||||
dispatch(start('confirm', name));
|
dispatch(start('confirm', name));
|
||||||
|
|
||||||
|
return getOwner(contract, name)
|
||||||
|
.then((owner) => {
|
||||||
|
if (owner.toLowerCase() !== account.address.toLowerCase()) {
|
||||||
|
throw new Error(`you are not the owner of "${name}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { confirmReverse } = contract.instance;
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
from: account.address
|
from: account.address
|
||||||
};
|
};
|
||||||
|
|
||||||
const values = [
|
const values = [
|
||||||
name
|
name
|
||||||
];
|
];
|
||||||
|
|
||||||
postTx(api, confirmReverse, options, values)
|
return postTx(api, confirmReverse, options, values);
|
||||||
|
})
|
||||||
.then((txHash) => {
|
.then((txHash) => {
|
||||||
dispatch(success('confirm'));
|
dispatch(success('confirm'));
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(`could not confirm reverse ${name}`);
|
if (err.type !== 'REQUEST_REJECTED') {
|
||||||
if (err) {
|
console.error(`error confirming ${name}`, err);
|
||||||
console.error(err.stack);
|
return dispatch(fail('confirm', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(fail('confirm'));
|
dispatch(fail('confirm'));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -17,24 +17,37 @@
|
|||||||
import { isAction, isStage } from '../util/actions';
|
import { isAction, isStage } from '../util/actions';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
error: null,
|
||||||
pending: false,
|
pending: false,
|
||||||
queue: []
|
queue: []
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (state = initialState, action) => {
|
export default (state = initialState, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'clearError':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (isAction('reverse', 'propose', action)) {
|
if (isAction('reverse', 'propose', action)) {
|
||||||
if (isStage('start', action)) {
|
if (isStage('start', action)) {
|
||||||
return {
|
return {
|
||||||
...state, pending: true,
|
...state, pending: true,
|
||||||
|
error: null,
|
||||||
queue: state.queue.concat({
|
queue: state.queue.concat({
|
||||||
action: 'propose',
|
action: 'propose',
|
||||||
name: action.name,
|
name: action.name,
|
||||||
address: action.address
|
address: action.address
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
} else if (isStage('success', action) || isStage('fail', action)) {
|
}
|
||||||
|
|
||||||
|
if (isStage('success', action) || isStage('fail', action)) {
|
||||||
return {
|
return {
|
||||||
...state, pending: false,
|
...state, pending: false,
|
||||||
|
error: action.error || null,
|
||||||
queue: state.queue.filter((e) =>
|
queue: state.queue.filter((e) =>
|
||||||
e.action === 'propose' &&
|
e.action === 'propose' &&
|
||||||
e.name === action.name &&
|
e.name === action.name &&
|
||||||
@ -48,14 +61,18 @@ export default (state = initialState, action) => {
|
|||||||
if (isStage('start', action)) {
|
if (isStage('start', action)) {
|
||||||
return {
|
return {
|
||||||
...state, pending: true,
|
...state, pending: true,
|
||||||
|
error: null,
|
||||||
queue: state.queue.concat({
|
queue: state.queue.concat({
|
||||||
action: 'confirm',
|
action: 'confirm',
|
||||||
name: action.name
|
name: action.name
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
} else if (isStage('success', action) || isStage('fail', action)) {
|
}
|
||||||
|
|
||||||
|
if (isStage('success', action) || isStage('fail', action)) {
|
||||||
return {
|
return {
|
||||||
...state, pending: false,
|
...state, pending: false,
|
||||||
|
error: action.error || null,
|
||||||
queue: state.queue.filter((e) =>
|
queue: state.queue.filter((e) =>
|
||||||
e.action === 'confirm' &&
|
e.action === 'confirm' &&
|
||||||
e.name === action.name
|
e.name === action.name
|
||||||
|
@ -37,3 +37,6 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
@ -21,17 +21,20 @@ import {
|
|||||||
Card, CardHeader, CardText, TextField, DropDownMenu, MenuItem, RaisedButton
|
Card, CardHeader, CardText, TextField, DropDownMenu, MenuItem, RaisedButton
|
||||||
} from 'material-ui';
|
} from 'material-ui';
|
||||||
|
|
||||||
|
import { nullableProptype } from '~/util/proptypes';
|
||||||
import { AddIcon, CheckIcon } from '~/ui/Icons';
|
import { AddIcon, CheckIcon } from '~/ui/Icons';
|
||||||
import { propose, confirm } from './actions';
|
import { clearError, confirm, propose } from './actions';
|
||||||
import styles from './reverse.css';
|
import styles from './reverse.css';
|
||||||
|
|
||||||
class Reverse extends Component {
|
class Reverse extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
error: nullableProptype(PropTypes.object.isRequired),
|
||||||
pending: PropTypes.bool.isRequired,
|
pending: PropTypes.bool.isRequired,
|
||||||
queue: PropTypes.array.isRequired,
|
queue: PropTypes.array.isRequired,
|
||||||
|
|
||||||
propose: PropTypes.func.isRequired,
|
clearError: PropTypes.func.isRequired,
|
||||||
confirm: PropTypes.func.isRequired
|
confirm: PropTypes.func.isRequired,
|
||||||
|
propose: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -77,6 +80,7 @@ class Reverse extends Component {
|
|||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
{ explanation }
|
{ explanation }
|
||||||
|
{ this.renderError() }
|
||||||
<div className={ styles.box }>
|
<div className={ styles.box }>
|
||||||
<DropDownMenu
|
<DropDownMenu
|
||||||
disabled={ pending }
|
disabled={ pending }
|
||||||
@ -108,6 +112,20 @@ class Reverse extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderError () {
|
||||||
|
const { error } = this.props;
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={ styles.error }>
|
||||||
|
<code>{ error.message }</code>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onNameChange = (e) => {
|
onNameChange = (e) => {
|
||||||
this.setState({ name: e.target.value });
|
this.setState({ name: e.target.value });
|
||||||
};
|
};
|
||||||
@ -129,9 +147,15 @@ class Reverse extends Component {
|
|||||||
this.props.confirm(name);
|
this.props.confirm(name);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
clearError = () => {
|
||||||
|
if (this.props.error) {
|
||||||
|
this.props.clearError();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state) => state.reverse;
|
const mapStateToProps = (state) => state.reverse;
|
||||||
const mapDispatchToProps = (dispatch) => bindActionCreators({ propose, confirm }, dispatch);
|
const mapDispatchToProps = (dispatch) => bindActionCreators({ clearError, confirm, propose }, dispatch);
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Reverse);
|
export default connect(mapStateToProps, mapDispatchToProps)(Reverse);
|
||||||
|
@ -20,31 +20,48 @@ import { connect } from 'react-redux';
|
|||||||
import Hash from './hash';
|
import Hash from './hash';
|
||||||
import etherscanUrl from '../util/etherscan-url';
|
import etherscanUrl from '../util/etherscan-url';
|
||||||
import IdentityIcon from '../IdentityIcon';
|
import IdentityIcon from '../IdentityIcon';
|
||||||
|
import { nullableProptype } from '~/util/proptypes';
|
||||||
|
|
||||||
import styles from './address.css';
|
import styles from './address.css';
|
||||||
|
|
||||||
class Address extends Component {
|
class Address extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
address: PropTypes.string.isRequired,
|
address: PropTypes.string.isRequired,
|
||||||
accounts: PropTypes.object.isRequired,
|
account: nullableProptype(PropTypes.object.isRequired),
|
||||||
contacts: PropTypes.object.isRequired,
|
|
||||||
isTestnet: PropTypes.bool.isRequired,
|
isTestnet: PropTypes.bool.isRequired,
|
||||||
key: PropTypes.string,
|
key: PropTypes.string,
|
||||||
shortenHash: PropTypes.bool
|
shortenHash: PropTypes.bool
|
||||||
}
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
key: 'address',
|
key: 'address',
|
||||||
shortenHash: true
|
shortenHash: true
|
||||||
}
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { address, accounts, contacts, isTestnet, key, shortenHash } = this.props;
|
const { address, key } = this.props;
|
||||||
|
|
||||||
let caption;
|
return (
|
||||||
if (accounts[address] || contacts[address]) {
|
<div
|
||||||
const name = (accounts[address] || contacts[address] || {}).name;
|
key={ key }
|
||||||
caption = (
|
className={ styles.container }
|
||||||
|
>
|
||||||
|
<IdentityIcon
|
||||||
|
address={ address }
|
||||||
|
className={ styles.align }
|
||||||
|
/>
|
||||||
|
{ this.renderCaption() }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCaption () {
|
||||||
|
const { address, account, isTestnet, shortenHash } = this.props;
|
||||||
|
|
||||||
|
if (account) {
|
||||||
|
const { name } = account;
|
||||||
|
|
||||||
|
return (
|
||||||
<a
|
<a
|
||||||
className={ styles.link }
|
className={ styles.link }
|
||||||
href={ etherscanUrl(address, isTestnet) }
|
href={ etherscanUrl(address, isTestnet) }
|
||||||
@ -58,8 +75,9 @@ class Address extends Component {
|
|||||||
</abbr>
|
</abbr>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
caption = (
|
|
||||||
|
return (
|
||||||
<code className={ styles.align }>
|
<code className={ styles.align }>
|
||||||
{ shortenHash ? (
|
{ shortenHash ? (
|
||||||
<Hash
|
<Hash
|
||||||
@ -70,29 +88,33 @@ class Address extends Component {
|
|||||||
</code>
|
</code>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
function mapStateToProps (initState, initProps) {
|
||||||
<div
|
const { accounts, contacts } = initState;
|
||||||
key={ key }
|
|
||||||
className={ styles.container }
|
const allAccounts = Object.assign({}, accounts.all, contacts);
|
||||||
>
|
|
||||||
<IdentityIcon
|
// Add lower case addresses to map
|
||||||
address={ address }
|
Object
|
||||||
className={ styles.align }
|
.keys(allAccounts)
|
||||||
/>
|
.forEach((address) => {
|
||||||
{ caption }
|
allAccounts[address.toLowerCase()] = allAccounts[address];
|
||||||
</div>
|
});
|
||||||
);
|
|
||||||
}
|
return (state, props) => {
|
||||||
|
const { isTestnet } = state;
|
||||||
|
const { address = '' } = props;
|
||||||
|
|
||||||
|
const account = allAccounts[address] || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
account,
|
||||||
|
isTestnet
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
// mapStateToProps
|
mapStateToProps
|
||||||
(state) => ({
|
|
||||||
accounts: state.accounts.all,
|
|
||||||
contacts: state.contacts,
|
|
||||||
isTestnet: state.isTestnet
|
|
||||||
}),
|
|
||||||
// mapDispatchToProps
|
|
||||||
null
|
|
||||||
)(Address);
|
)(Address);
|
||||||
|
@ -23,10 +23,20 @@ const styles = {
|
|||||||
border: '1px solid #777'
|
border: '1px solid #777'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (address) => (
|
export default (address) => {
|
||||||
|
if (!address || /^(0x)?0*$/.test(address)) {
|
||||||
|
return (
|
||||||
|
<code>
|
||||||
|
No image
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<img
|
<img
|
||||||
src={ `${parityNode}/${address}/` }
|
src={ `${parityNode}/${address}/` }
|
||||||
alt={ address }
|
alt={ address }
|
||||||
style={ styles }
|
style={ styles }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
@ -19,7 +19,7 @@ export const isAction = (ns, type, action) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isStage = (stage, action) => {
|
export const isStage = (stage, action) => {
|
||||||
return action.type.slice(-1 - stage.length) === ` ${stage}`;
|
return (new RegExp(`${stage}$`)).test(action.type);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addToQueue = (queue, action, name) => {
|
export const addToQueue = (queue, action, name) => {
|
||||||
@ -27,5 +27,5 @@ export const addToQueue = (queue, action, name) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const removeFromQueue = (queue, action, name) => {
|
export const removeFromQueue = (queue, action, name) => {
|
||||||
return queue.filter((e) => e.action === action && e.name === name);
|
return queue.filter((e) => !(e.action === action && e.name === name));
|
||||||
};
|
};
|
||||||
|
@ -24,12 +24,6 @@ const postTx = (api, method, opt = {}, values = []) => {
|
|||||||
})
|
})
|
||||||
.then((reqId) => {
|
.then((reqId) => {
|
||||||
return api.pollMethod('parity_checkRequest', reqId);
|
return api.pollMethod('parity_checkRequest', reqId);
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (err && err.type === 'REQUEST_REJECTED') {
|
|
||||||
throw new Error('The request has been rejected.');
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import { showSnackbar } from './snackbarActions';
|
import { showSnackbar } from './snackbarActions';
|
||||||
|
import { DEFAULT_NETCHAIN } from './statusReducer';
|
||||||
|
|
||||||
export default class ChainMiddleware {
|
export default class ChainMiddleware {
|
||||||
toMiddleware () {
|
toMiddleware () {
|
||||||
@ -23,11 +24,11 @@ export default class ChainMiddleware {
|
|||||||
const { collection } = action;
|
const { collection } = action;
|
||||||
|
|
||||||
if (collection && collection.netChain) {
|
if (collection && collection.netChain) {
|
||||||
const chain = collection.netChain;
|
const newChain = collection.netChain;
|
||||||
const { nodeStatus } = store.getState();
|
const { nodeStatus } = store.getState();
|
||||||
|
|
||||||
if (chain !== nodeStatus.netChain) {
|
if (newChain !== nodeStatus.netChain && nodeStatus.netChain !== DEFAULT_NETCHAIN) {
|
||||||
store.dispatch(showSnackbar(`Switched to ${chain}. Please reload the page.`, 5000));
|
store.dispatch(showSnackbar(`Switched to ${newChain}. Please reload the page.`, 60000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 BigNumber from 'bignumber.js';
|
||||||
import { handleActions } from 'redux-actions';
|
import { handleActions } from 'redux-actions';
|
||||||
|
|
||||||
|
const DEFAULT_NETCHAIN = '(unknown)';
|
||||||
const initialState = {
|
const initialState = {
|
||||||
blockNumber: new BigNumber(0),
|
blockNumber: new BigNumber(0),
|
||||||
blockTimestamp: new Date(),
|
blockTimestamp: new Date(),
|
||||||
@ -32,7 +33,7 @@ const initialState = {
|
|||||||
gasLimit: new BigNumber(0),
|
gasLimit: new BigNumber(0),
|
||||||
hashrate: new BigNumber(0),
|
hashrate: new BigNumber(0),
|
||||||
minGasPrice: new BigNumber(0),
|
minGasPrice: new BigNumber(0),
|
||||||
netChain: 'ropsten',
|
netChain: DEFAULT_NETCHAIN,
|
||||||
netPeers: {
|
netPeers: {
|
||||||
active: new BigNumber(0),
|
active: new BigNumber(0),
|
||||||
connected: new BigNumber(0),
|
connected: new BigNumber(0),
|
||||||
@ -82,3 +83,8 @@ export default handleActions({
|
|||||||
return Object.assign({}, state, { refreshStatus });
|
return Object.assign({}, state, { refreshStatus });
|
||||||
}
|
}
|
||||||
}, initialState);
|
}, initialState);
|
||||||
|
|
||||||
|
export {
|
||||||
|
DEFAULT_NETCHAIN,
|
||||||
|
initialState
|
||||||
|
};
|
||||||
|
@ -50,8 +50,7 @@ export default class Actionbar extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolbarGroup
|
<ToolbarGroup className={ styles.toolbuttons }>
|
||||||
className={ styles.toolbuttons }>
|
|
||||||
{ buttons }
|
{ buttons }
|
||||||
</ToolbarGroup>
|
</ToolbarGroup>
|
||||||
);
|
);
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
@ -39,7 +40,12 @@ class BlockStatus extends Component {
|
|||||||
if (!syncing) {
|
if (!syncing) {
|
||||||
return (
|
return (
|
||||||
<div className={ styles.blockNumber }>
|
<div className={ styles.blockNumber }>
|
||||||
{ blockNumber.toFormat() } best block
|
<FormattedMessage
|
||||||
|
id='ui.blockStatus.bestBlock'
|
||||||
|
defaultMessage='{blockNumber} best block'
|
||||||
|
values={ {
|
||||||
|
blockNumber: blockNumber.toFormat()
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -47,26 +53,45 @@ class BlockStatus extends Component {
|
|||||||
if (syncing.warpChunksAmount && syncing.warpChunksProcessed && !syncing.warpChunksAmount.eq(syncing.warpChunksProcessed)) {
|
if (syncing.warpChunksAmount && syncing.warpChunksProcessed && !syncing.warpChunksAmount.eq(syncing.warpChunksProcessed)) {
|
||||||
return (
|
return (
|
||||||
<div className={ styles.syncStatus }>
|
<div className={ styles.syncStatus }>
|
||||||
{ syncing.warpChunksProcessed.mul(100).div(syncing.warpChunksAmount).toFormat(2) }% warp restore
|
<FormattedMessage
|
||||||
|
id='ui.blockStatus.warpRestore'
|
||||||
|
defaultMessage='{percentage}% warp restore'
|
||||||
|
values={ {
|
||||||
|
percentage: syncing.warpChunksProcessed.mul(100).div(syncing.warpChunksAmount).toFormat(2)
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let syncStatus = null;
|
||||||
let warpStatus = null;
|
let warpStatus = null;
|
||||||
|
|
||||||
|
if (syncing.currentBlock && syncing.highestBlock) {
|
||||||
|
syncStatus = (
|
||||||
|
<span>
|
||||||
|
<FormattedMessage
|
||||||
|
id='ui.blockStatus.syncStatus'
|
||||||
|
defaultMessage='{currentBlock}/{highestBlock} syncing'
|
||||||
|
values={ {
|
||||||
|
currentBlock: syncing.currentBlock.toFormat(),
|
||||||
|
highestBlock: syncing.highestBlock.toFormat()
|
||||||
|
} } />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (syncing.blockGap) {
|
if (syncing.blockGap) {
|
||||||
const [first, last] = syncing.blockGap;
|
const [first, last] = syncing.blockGap;
|
||||||
|
|
||||||
warpStatus = (
|
warpStatus = (
|
||||||
<span>, { first.mul(100).div(last).toFormat(2) }% historic</span>
|
<span>
|
||||||
);
|
<FormattedMessage
|
||||||
}
|
id='ui.blockStatus.warpStatus'
|
||||||
|
defaultMessage=', {percentage}% historic'
|
||||||
let syncStatus = null;
|
values={ {
|
||||||
|
percentage: first.mul(100).div(last).toFormat(2)
|
||||||
if (syncing && syncing.currentBlock && syncing.highestBlock) {
|
} } />
|
||||||
syncStatus = (
|
</span>
|
||||||
<span>{ syncing.currentBlock.toFormat() }/{ syncing.highestBlock.toFormat() } syncing</span>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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';
|
import Button from './button';
|
||||||
|
|
||||||
function renderShallow (props) {
|
function render (props = {}) {
|
||||||
|
if (props && props.label === undefined) {
|
||||||
|
props.label = 'test';
|
||||||
|
}
|
||||||
|
|
||||||
return shallow(
|
return shallow(
|
||||||
<Button { ...props } />
|
<Button { ...props } />
|
||||||
);
|
);
|
||||||
@ -28,11 +32,11 @@ function renderShallow (props) {
|
|||||||
describe('ui/Button', () => {
|
describe('ui/Button', () => {
|
||||||
describe('rendering', () => {
|
describe('rendering', () => {
|
||||||
it('renders defaults', () => {
|
it('renders defaults', () => {
|
||||||
expect(renderShallow()).to.be.ok;
|
expect(render()).to.be.ok;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders with the specified className', () => {
|
it('renders with the specified className', () => {
|
||||||
expect(renderShallow({ className: 'testClass' })).to.have.className('testClass');
|
expect(render({ className: 'testClass' })).to.have.className('testClass');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -25,7 +25,7 @@ import styles from './certifications.css';
|
|||||||
|
|
||||||
class Certifications extends Component {
|
class Certifications extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: PropTypes.string.isRequired,
|
address: PropTypes.string.isRequired,
|
||||||
certifications: PropTypes.array.isRequired,
|
certifications: PropTypes.array.isRequired,
|
||||||
dappsUrl: PropTypes.string.isRequired
|
dappsUrl: PropTypes.string.isRequired
|
||||||
}
|
}
|
||||||
@ -60,10 +60,10 @@ class Certifications extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps (_, initProps) {
|
function mapStateToProps (_, initProps) {
|
||||||
const { account } = initProps;
|
const { address } = initProps;
|
||||||
|
|
||||||
return (state) => {
|
return (state) => {
|
||||||
const certifications = state.certifications[account] || [];
|
const certifications = state.certifications[address] || [];
|
||||||
const dappsUrl = state.api.dappsUrl;
|
const dappsUrl = state.api.dappsUrl;
|
||||||
|
|
||||||
return { certifications, dappsUrl };
|
return { certifications, dappsUrl };
|
||||||
|
@ -15,16 +15,27 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import ActionDone from 'material-ui/svg-icons/action/done';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import ContentClear from 'material-ui/svg-icons/content/clear';
|
|
||||||
|
|
||||||
import { nodeOrStringProptype } from '~/util/proptypes';
|
import { nodeOrStringProptype } from '~/util/proptypes';
|
||||||
|
|
||||||
import Button from '../Button';
|
import Button from '../Button';
|
||||||
import Modal from '../Modal';
|
import Modal from '../Modal';
|
||||||
|
import { CancelIcon, CheckIcon } from '../Icons';
|
||||||
|
|
||||||
import styles from './confirmDialog.css';
|
import styles from './confirmDialog.css';
|
||||||
|
|
||||||
|
const DEFAULT_NO = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='ui.confirmDialog.no'
|
||||||
|
defaultMessage='no' />
|
||||||
|
);
|
||||||
|
const DEFAULT_YES = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='ui.confirmDialog.yes'
|
||||||
|
defaultMessage='yes' />
|
||||||
|
);
|
||||||
|
|
||||||
export default class ConfirmDialog extends Component {
|
export default class ConfirmDialog extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
@ -33,10 +44,10 @@ export default class ConfirmDialog extends Component {
|
|||||||
iconDeny: PropTypes.node,
|
iconDeny: PropTypes.node,
|
||||||
labelConfirm: PropTypes.string,
|
labelConfirm: PropTypes.string,
|
||||||
labelDeny: PropTypes.string,
|
labelDeny: PropTypes.string,
|
||||||
title: nodeOrStringProptype().isRequired,
|
|
||||||
visible: PropTypes.bool.isRequired,
|
|
||||||
onConfirm: PropTypes.func.isRequired,
|
onConfirm: PropTypes.func.isRequired,
|
||||||
onDeny: PropTypes.func.isRequired
|
onDeny: PropTypes.func.isRequired,
|
||||||
|
title: nodeOrStringProptype().isRequired,
|
||||||
|
visible: PropTypes.bool.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
@ -60,12 +71,12 @@ export default class ConfirmDialog extends Component {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
<Button
|
<Button
|
||||||
label={ labelDeny || 'no' }
|
icon={ iconDeny || <CancelIcon /> }
|
||||||
icon={ iconDeny || <ContentClear /> }
|
label={ labelDeny || DEFAULT_NO }
|
||||||
onClick={ onDeny } />,
|
onClick={ onDeny } />,
|
||||||
<Button
|
<Button
|
||||||
label={ labelConfirm || 'yes' }
|
icon={ iconConfirm || <CheckIcon /> }
|
||||||
icon={ iconConfirm || <ActionDone /> }
|
label={ labelConfirm || DEFAULT_YES }
|
||||||
onClick={ onConfirm } />
|
onClick={ onConfirm } />
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
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';
|
import Container from './container';
|
||||||
|
|
||||||
function renderShallow (props) {
|
function render (props) {
|
||||||
return shallow(
|
return shallow(
|
||||||
<Container { ...props } />
|
<Container { ...props } />
|
||||||
);
|
);
|
||||||
@ -28,11 +28,24 @@ function renderShallow (props) {
|
|||||||
describe('ui/Container', () => {
|
describe('ui/Container', () => {
|
||||||
describe('rendering', () => {
|
describe('rendering', () => {
|
||||||
it('renders defaults', () => {
|
it('renders defaults', () => {
|
||||||
expect(renderShallow()).to.be.ok;
|
expect(render()).to.be.ok;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders with the specified className', () => {
|
it('renders with the specified className', () => {
|
||||||
expect(renderShallow({ className: 'testClass' })).to.have.className('testClass');
|
expect(render({ className: 'testClass' })).to.have.className('testClass');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sections', () => {
|
||||||
|
it('renders the Card', () => {
|
||||||
|
expect(render().find('Card')).to.have.length(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the Title', () => {
|
||||||
|
const title = render({ title: 'title' }).find('Title');
|
||||||
|
|
||||||
|
expect(title).to.have.length(1);
|
||||||
|
expect(title.props().title).to.equal('title');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -60,6 +60,7 @@ class AddressSelect extends Component {
|
|||||||
// Optional props
|
// Optional props
|
||||||
allowCopy: PropTypes.bool,
|
allowCopy: PropTypes.bool,
|
||||||
allowInput: PropTypes.bool,
|
allowInput: PropTypes.bool,
|
||||||
|
className: PropTypes.string,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
error: nodeOrStringProptype(),
|
error: nodeOrStringProptype(),
|
||||||
hint: nodeOrStringProptype(),
|
hint: nodeOrStringProptype(),
|
||||||
@ -123,13 +124,14 @@ class AddressSelect extends Component {
|
|||||||
|
|
||||||
renderInput () {
|
renderInput () {
|
||||||
const { focused } = this.state;
|
const { focused } = this.state;
|
||||||
const { accountsInfo, allowCopy, disabled, error, hint, label, readOnly, value } = this.props;
|
const { accountsInfo, allowCopy, className, disabled, error, hint, label, readOnly, value } = this.props;
|
||||||
|
|
||||||
const input = (
|
const input = (
|
||||||
<InputAddress
|
<InputAddress
|
||||||
accountsInfo={ accountsInfo }
|
accountsInfo={ accountsInfo }
|
||||||
allowCopy={ allowCopy }
|
allowCopy={ allowCopy }
|
||||||
disabled={ disabled }
|
className={ className }
|
||||||
|
disabled={ disabled || readOnly }
|
||||||
error={ error }
|
error={ error }
|
||||||
hint={ hint }
|
hint={ hint }
|
||||||
focused={ focused }
|
focused={ focused }
|
||||||
@ -215,8 +217,9 @@ class AddressSelect extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { address, addressError } = validateAddress(inputValue);
|
const { address, addressError } = validateAddress(inputValue);
|
||||||
|
const { registryValues } = this.store;
|
||||||
|
|
||||||
if (addressError) {
|
if (addressError || registryValues.length > 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,8 @@ import { FormattedMessage } from 'react-intl';
|
|||||||
import Contracts from '~/contracts';
|
import Contracts from '~/contracts';
|
||||||
import { sha3 } from '~/api/util/sha3';
|
import { sha3 } from '~/api/util/sha3';
|
||||||
|
|
||||||
|
const ZERO = /^(0x)?0*$/;
|
||||||
|
|
||||||
export default class AddressSelectStore {
|
export default class AddressSelectStore {
|
||||||
|
|
||||||
@observable values = [];
|
@observable values = [];
|
||||||
@ -38,41 +40,75 @@ export default class AddressSelectStore {
|
|||||||
registry
|
registry
|
||||||
.getContract('emailverification')
|
.getContract('emailverification')
|
||||||
.then((emailVerification) => {
|
.then((emailVerification) => {
|
||||||
this.regLookups.push({
|
this.regLookups.push((email) => {
|
||||||
lookup: (value) => {
|
|
||||||
return emailVerification
|
return emailVerification
|
||||||
.instance
|
.instance
|
||||||
.reverse.call({}, [ sha3(value) ]);
|
.reverse
|
||||||
},
|
.call({}, [ sha3(email) ])
|
||||||
describe: (value) => (
|
.then((address) => {
|
||||||
|
return {
|
||||||
|
address,
|
||||||
|
description: (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='addressSelect.fromEmail'
|
id='addressSelect.fromEmail'
|
||||||
defaultMessage='Verified using email {value}'
|
defaultMessage='Verified using email {email}'
|
||||||
values={ {
|
values={ {
|
||||||
value
|
email
|
||||||
} }
|
} }
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
};
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
registry
|
registry
|
||||||
.getInstance()
|
.getInstance()
|
||||||
.then((registryInstance) => {
|
.then((registryInstance) => {
|
||||||
this.regLookups.push({
|
this.regLookups.push((name) => {
|
||||||
lookup: (value) => {
|
|
||||||
return registryInstance
|
return registryInstance
|
||||||
.getAddress.call({}, [ sha3(value), 'A' ]);
|
.getAddress
|
||||||
},
|
.call({}, [ sha3(name), 'A' ])
|
||||||
describe: (value) => (
|
.then((address) => {
|
||||||
|
return {
|
||||||
|
address,
|
||||||
|
name,
|
||||||
|
description: (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='addressSelect.fromRegistry'
|
id='addressSelect.fromRegistry'
|
||||||
defaultMessage='{value} (from registry)'
|
defaultMessage='{name} (from registry)'
|
||||||
values={ {
|
values={ {
|
||||||
value
|
name
|
||||||
} }
|
} }
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.regLookups.push((address) => {
|
||||||
|
return registryInstance
|
||||||
|
.reverse
|
||||||
|
.call({}, [ address ])
|
||||||
|
.then((name) => {
|
||||||
|
if (!name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
address,
|
||||||
|
name,
|
||||||
|
description: (
|
||||||
|
<FormattedMessage
|
||||||
|
id='addressSelect.fromRegistry'
|
||||||
|
defaultMessage='{name} (from registry)'
|
||||||
|
values={ {
|
||||||
|
name
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -149,32 +185,30 @@ export default class AddressSelectStore {
|
|||||||
// Registries Lookup
|
// Registries Lookup
|
||||||
this.registryValues = [];
|
this.registryValues = [];
|
||||||
|
|
||||||
const lookups = this.regLookups.map((regLookup) => regLookup.lookup(value));
|
const lookups = this.regLookups.map((regLookup) => regLookup(value));
|
||||||
|
|
||||||
Promise
|
Promise
|
||||||
.all(lookups)
|
.all(lookups)
|
||||||
.then((results) => {
|
.then((results) => {
|
||||||
return results
|
return results
|
||||||
.map((result, index) => {
|
.filter((result) => result && !ZERO.test(result.address));
|
||||||
if (/^(0x)?0*$/.test(result)) {
|
})
|
||||||
return;
|
.then((results) => {
|
||||||
}
|
this.registryValues = results
|
||||||
|
.map((result) => {
|
||||||
const lowercaseResult = result.toLowerCase();
|
const lowercaseAddress = result.address.toLowerCase();
|
||||||
|
|
||||||
const account = flatMap(this.initValues, (cat) => cat.values)
|
const account = flatMap(this.initValues, (cat) => cat.values)
|
||||||
.find((account) => account.address.toLowerCase() === lowercaseResult);
|
.find((account) => account.address.toLowerCase() === lowercaseAddress);
|
||||||
|
|
||||||
return {
|
if (account && account.name) {
|
||||||
description: this.regLookups[index].describe(value),
|
result.name = account.name;
|
||||||
address: result,
|
} else if (!result.name) {
|
||||||
name: account && account.name || value
|
result.name = value;
|
||||||
};
|
}
|
||||||
})
|
|
||||||
.filter((data) => data);
|
return result;
|
||||||
})
|
});
|
||||||
.then((registryValues) => {
|
|
||||||
this.registryValues = registryValues;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +75,12 @@ class InputAddress extends Component {
|
|||||||
containerClasses.push(styles.small);
|
containerClasses.push(styles.small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const props = {};
|
||||||
|
|
||||||
|
if (!readOnly && !disabled) {
|
||||||
|
props.focused = focused;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ containerClasses.join(' ') }>
|
<div className={ containerClasses.join(' ') }>
|
||||||
<Input
|
<Input
|
||||||
@ -82,7 +88,6 @@ class InputAddress extends Component {
|
|||||||
className={ classes.join(' ') }
|
className={ classes.join(' ') }
|
||||||
disabled={ disabled }
|
disabled={ disabled }
|
||||||
error={ error }
|
error={ error }
|
||||||
focused={ focused }
|
|
||||||
hideUnderline={ hideUnderline }
|
hideUnderline={ hideUnderline }
|
||||||
hint={ hint }
|
hint={ hint }
|
||||||
label={ label }
|
label={ label }
|
||||||
@ -96,7 +101,9 @@ class InputAddress extends Component {
|
|||||||
text && account
|
text && account
|
||||||
? account.name
|
? account.name
|
||||||
: (nullName || value)
|
: (nullName || value)
|
||||||
} />
|
}
|
||||||
|
{ ...props }
|
||||||
|
/>
|
||||||
{ icon }
|
{ icon }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -27,6 +27,7 @@ class InputAddressSelect extends Component {
|
|||||||
contracts: PropTypes.object.isRequired,
|
contracts: PropTypes.object.isRequired,
|
||||||
|
|
||||||
allowCopy: PropTypes.bool,
|
allowCopy: PropTypes.bool,
|
||||||
|
className: PropTypes.string,
|
||||||
error: PropTypes.string,
|
error: PropTypes.string,
|
||||||
hint: PropTypes.string,
|
hint: PropTypes.string,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
@ -36,13 +37,14 @@ class InputAddressSelect extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { accounts, allowCopy, contacts, contracts, label, hint, error, value, onChange, readOnly } = this.props;
|
const { accounts, allowCopy, className, contacts, contracts, label, hint, error, value, onChange, readOnly } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AddressSelect
|
<AddressSelect
|
||||||
allowCopy={ allowCopy }
|
allowCopy={ allowCopy }
|
||||||
allowInput
|
allowInput
|
||||||
accounts={ accounts }
|
accounts={ accounts }
|
||||||
|
className={ className }
|
||||||
contacts={ contacts }
|
contacts={ contacts }
|
||||||
contracts={ contracts }
|
contracts={ contracts }
|
||||||
error={ error }
|
error={ error }
|
||||||
|
@ -41,6 +41,7 @@ export default class TypedInput extends Component {
|
|||||||
|
|
||||||
accounts: PropTypes.object,
|
accounts: PropTypes.object,
|
||||||
allowCopy: PropTypes.bool,
|
allowCopy: PropTypes.bool,
|
||||||
|
className: PropTypes.string,
|
||||||
error: PropTypes.any,
|
error: PropTypes.any,
|
||||||
hint: PropTypes.string,
|
hint: PropTypes.string,
|
||||||
isEth: PropTypes.bool,
|
isEth: PropTypes.bool,
|
||||||
@ -91,7 +92,7 @@ export default class TypedInput extends Component {
|
|||||||
const { type } = param;
|
const { type } = param;
|
||||||
|
|
||||||
if (type === ABI_TYPES.ARRAY) {
|
if (type === ABI_TYPES.ARRAY) {
|
||||||
const { accounts, label, value = param.default } = this.props;
|
const { accounts, className, label, value = param.default } = this.props;
|
||||||
const { subtype, length } = param;
|
const { subtype, length } = param;
|
||||||
|
|
||||||
const fixedLength = !!length;
|
const fixedLength = !!length;
|
||||||
@ -107,6 +108,7 @@ export default class TypedInput extends Component {
|
|||||||
<TypedInput
|
<TypedInput
|
||||||
accounts={ accounts }
|
accounts={ accounts }
|
||||||
allowCopy={ allowCopy }
|
allowCopy={ allowCopy }
|
||||||
|
className={ className }
|
||||||
key={ `${subtype.type}_${index}` }
|
key={ `${subtype.type}_${index}` }
|
||||||
onChange={ onChange }
|
onChange={ onChange }
|
||||||
param={ subtype }
|
param={ subtype }
|
||||||
@ -236,17 +238,34 @@ export default class TypedInput extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNumberValue (value) {
|
||||||
|
if (!value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { readOnly } = this.props;
|
||||||
|
|
||||||
|
const rawValue = typeof value === 'string'
|
||||||
|
? value.replace(/,/g, '')
|
||||||
|
: value;
|
||||||
|
|
||||||
|
const bnValue = new BigNumber(rawValue);
|
||||||
|
|
||||||
|
return readOnly
|
||||||
|
? bnValue.toFormat()
|
||||||
|
: bnValue.toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
renderInteger (value = this.props.value, onChange = this.onChange) {
|
renderInteger (value = this.props.value, onChange = this.onChange) {
|
||||||
const { allowCopy, label, error, hint, min, max, readOnly } = this.props;
|
const { allowCopy, className, label, error, hint, min, max, readOnly } = this.props;
|
||||||
const param = this.getParam();
|
const param = this.getParam();
|
||||||
|
|
||||||
const realValue = value
|
const realValue = this.getNumberValue(value);
|
||||||
? (new BigNumber(value))[readOnly ? 'toFormat' : 'toNumber']()
|
|
||||||
: value;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
allowCopy={ allowCopy }
|
allowCopy={ allowCopy }
|
||||||
|
className={ className }
|
||||||
label={ label }
|
label={ label }
|
||||||
hint={ hint }
|
hint={ hint }
|
||||||
value={ realValue }
|
value={ realValue }
|
||||||
@ -269,16 +288,15 @@ export default class TypedInput extends Component {
|
|||||||
* @see https://github.com/facebook/react/issues/1549
|
* @see https://github.com/facebook/react/issues/1549
|
||||||
*/
|
*/
|
||||||
renderFloat (value = this.props.value, onChange = this.onChange) {
|
renderFloat (value = this.props.value, onChange = this.onChange) {
|
||||||
const { allowCopy, label, error, hint, min, max, readOnly } = this.props;
|
const { allowCopy, className, label, error, hint, min, max, readOnly } = this.props;
|
||||||
const param = this.getParam();
|
const param = this.getParam();
|
||||||
|
|
||||||
const realValue = value
|
const realValue = this.getNumberValue(value);
|
||||||
? (new BigNumber(value))[readOnly ? 'toFormat' : 'toNumber']()
|
|
||||||
: value;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
allowCopy={ allowCopy }
|
allowCopy={ allowCopy }
|
||||||
|
className={ className }
|
||||||
label={ label }
|
label={ label }
|
||||||
hint={ hint }
|
hint={ hint }
|
||||||
value={ realValue }
|
value={ realValue }
|
||||||
@ -293,11 +311,12 @@ export default class TypedInput extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderDefault () {
|
renderDefault () {
|
||||||
const { allowCopy, label, value, error, hint, readOnly } = this.props;
|
const { allowCopy, className, label, value, error, hint, readOnly } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
allowCopy={ allowCopy }
|
allowCopy={ allowCopy }
|
||||||
|
className={ className }
|
||||||
label={ label }
|
label={ label }
|
||||||
hint={ hint }
|
hint={ hint }
|
||||||
value={ value }
|
value={ value }
|
||||||
@ -309,12 +328,13 @@ export default class TypedInput extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderAddress () {
|
renderAddress () {
|
||||||
const { accounts, allowCopy, label, value, error, hint, readOnly } = this.props;
|
const { accounts, allowCopy, className, label, value, error, hint, readOnly } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputAddressSelect
|
<InputAddressSelect
|
||||||
allowCopy={ allowCopy }
|
allowCopy={ allowCopy }
|
||||||
accounts={ accounts }
|
accounts={ accounts }
|
||||||
|
className={ className }
|
||||||
error={ error }
|
error={ error }
|
||||||
hint={ hint }
|
hint={ hint }
|
||||||
label={ label }
|
label={ label }
|
||||||
@ -326,7 +346,7 @@ export default class TypedInput extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderBoolean () {
|
renderBoolean () {
|
||||||
const { allowCopy, label, value, error, hint, readOnly } = this.props;
|
const { allowCopy, className, label, value, error, hint, readOnly } = this.props;
|
||||||
|
|
||||||
if (readOnly) {
|
if (readOnly) {
|
||||||
return this.renderDefault();
|
return this.renderDefault();
|
||||||
@ -346,6 +366,7 @@ export default class TypedInput extends Component {
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
allowCopy={ allowCopy }
|
allowCopy={ allowCopy }
|
||||||
|
className={ className }
|
||||||
error={ error }
|
error={ error }
|
||||||
hint={ hint }
|
hint={ hint }
|
||||||
label={ label }
|
label={ label }
|
||||||
|
@ -29,6 +29,7 @@ const api = {
|
|||||||
|
|
||||||
const store = {
|
const store = {
|
||||||
estimated: '123',
|
estimated: '123',
|
||||||
|
histogram: {},
|
||||||
priceDefault: '456',
|
priceDefault: '456',
|
||||||
totalValue: '789',
|
totalValue: '789',
|
||||||
setGas: sinon.stub(),
|
setGas: sinon.stub(),
|
||||||
|
@ -18,28 +18,42 @@ import AddIcon from 'material-ui/svg-icons/content/add';
|
|||||||
import CancelIcon from 'material-ui/svg-icons/content/clear';
|
import CancelIcon from 'material-ui/svg-icons/content/clear';
|
||||||
import CheckIcon from 'material-ui/svg-icons/navigation/check';
|
import CheckIcon from 'material-ui/svg-icons/navigation/check';
|
||||||
import CloseIcon from 'material-ui/svg-icons/navigation/close';
|
import CloseIcon from 'material-ui/svg-icons/navigation/close';
|
||||||
|
import CompareIcon from 'material-ui/svg-icons/action/compare-arrows';
|
||||||
|
import ComputerIcon from 'material-ui/svg-icons/hardware/desktop-mac';
|
||||||
import ContractIcon from 'material-ui/svg-icons/action/code';
|
import ContractIcon from 'material-ui/svg-icons/action/code';
|
||||||
|
import DashboardIcon from 'material-ui/svg-icons/action/dashboard';
|
||||||
|
import DeleteIcon from 'material-ui/svg-icons/action/delete';
|
||||||
import DoneIcon from 'material-ui/svg-icons/action/done-all';
|
import DoneIcon from 'material-ui/svg-icons/action/done-all';
|
||||||
import LockedIcon from 'material-ui/svg-icons/action/lock-outline';
|
import EditIcon from 'material-ui/svg-icons/content/create';
|
||||||
|
import LockedIcon from 'material-ui/svg-icons/action/lock';
|
||||||
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
|
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
|
||||||
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
|
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
|
||||||
import SaveIcon from 'material-ui/svg-icons/content/save';
|
import SaveIcon from 'material-ui/svg-icons/content/save';
|
||||||
import SendIcon from 'material-ui/svg-icons/content/send';
|
import SendIcon from 'material-ui/svg-icons/content/send';
|
||||||
import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
|
import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
|
||||||
|
import VerifyIcon from 'material-ui/svg-icons/action/verified-user';
|
||||||
import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
|
import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
|
||||||
|
import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AddIcon,
|
AddIcon,
|
||||||
CancelIcon,
|
CancelIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
|
CompareIcon,
|
||||||
|
ComputerIcon,
|
||||||
ContractIcon,
|
ContractIcon,
|
||||||
|
DashboardIcon,
|
||||||
|
DeleteIcon,
|
||||||
DoneIcon,
|
DoneIcon,
|
||||||
|
EditIcon,
|
||||||
LockedIcon,
|
LockedIcon,
|
||||||
NextIcon,
|
NextIcon,
|
||||||
PrevIcon,
|
PrevIcon,
|
||||||
SaveIcon,
|
SaveIcon,
|
||||||
SendIcon,
|
SendIcon,
|
||||||
SnoozeIcon,
|
SnoozeIcon,
|
||||||
VisibleIcon
|
VerifyIcon,
|
||||||
|
VisibleIcon,
|
||||||
|
VpnIcon
|
||||||
};
|
};
|
||||||
|
@ -34,8 +34,8 @@ class IdentityIcon extends Component {
|
|||||||
button: PropTypes.bool,
|
button: PropTypes.bool,
|
||||||
center: PropTypes.bool,
|
center: PropTypes.bool,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
inline: PropTypes.bool,
|
|
||||||
images: PropTypes.object.isRequired,
|
images: PropTypes.object.isRequired,
|
||||||
|
inline: PropTypes.bool,
|
||||||
padded: PropTypes.bool,
|
padded: PropTypes.bool,
|
||||||
tiny: PropTypes.bool
|
tiny: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
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/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
import { isNullAddress } from '~/util/validation';
|
import { isNullAddress } from '~/util/validation';
|
||||||
import ShortenedHash from '../ShortenedHash';
|
import ShortenedHash from '../ShortenedHash';
|
||||||
|
|
||||||
const defaultName = 'UNNAMED';
|
const defaultName = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='ui.identityName.unnamed'
|
||||||
|
defaultMessage='UNNAMED' />
|
||||||
|
);
|
||||||
|
const defaultNameNull = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='ui.identityName.null'
|
||||||
|
defaultMessage='NULL' />
|
||||||
|
);
|
||||||
|
|
||||||
class IdentityName extends Component {
|
class IdentityName extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -43,7 +53,7 @@ class IdentityName extends Component {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nullName = isNullAddress(address) ? 'null' : null;
|
const nullName = isNullAddress(address) ? defaultNameNull : null;
|
||||||
const addressFallback = nullName || (shorten ? (<ShortenedHash data={ address } />) : address);
|
const addressFallback = nullName || (shorten ? (<ShortenedHash data={ address } />) : address);
|
||||||
const fallback = unknown ? defaultName : addressFallback;
|
const fallback = unknown ? defaultName : addressFallback;
|
||||||
const isUuid = account && account.name === account.uuid;
|
const isUuid = account && account.name === account.uuid;
|
||||||
|
@ -14,8 +14,10 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
||||||
import IdentityName from './identityName';
|
import IdentityName from './identityName';
|
||||||
@ -44,9 +46,11 @@ const STORE = {
|
|||||||
|
|
||||||
function render (props) {
|
function render (props) {
|
||||||
return mount(
|
return mount(
|
||||||
|
<IntlProvider locale='en'>
|
||||||
<IdentityName
|
<IdentityName
|
||||||
store={ STORE }
|
store={ STORE }
|
||||||
{ ...props } />
|
{ ...props } />
|
||||||
|
</IntlProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +78,7 @@ describe('ui/IdentityName', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders 0x000...000 as null', () => {
|
it('renders 0x000...000 as null', () => {
|
||||||
expect(render({ address: ADDR_NULL }).text()).to.equal('null');
|
expect(render({ address: ADDR_NULL }).text()).to.equal('NULL');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -14,12 +14,11 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import CircularProgress from 'material-ui/CircularProgress';
|
import CircularProgress from 'material-ui/CircularProgress';
|
||||||
|
|
||||||
import { Input, InputAddress } from '../Form';
|
import { TypedInput, InputAddress } from '../Form';
|
||||||
import MethodDecodingStore from './methodDecodingStore';
|
import MethodDecodingStore from './methodDecodingStore';
|
||||||
|
|
||||||
import styles from './methodDecoding.css';
|
import styles from './methodDecoding.css';
|
||||||
@ -245,6 +244,7 @@ class MethodDecoding extends Component {
|
|||||||
|
|
||||||
renderDeploy () {
|
renderDeploy () {
|
||||||
const { historic, transaction } = this.props;
|
const { historic, transaction } = this.props;
|
||||||
|
const { methodInputs } = this.state;
|
||||||
|
|
||||||
if (!historic) {
|
if (!historic) {
|
||||||
return (
|
return (
|
||||||
@ -261,6 +261,14 @@ class MethodDecoding extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ this.renderAddressName(transaction.creates, false) }
|
{ this.renderAddressName(transaction.creates, false) }
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{ methodInputs && methodInputs.length ? 'with the following parameters:' : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={ styles.inputs }>
|
||||||
|
{ this.renderInputs() }
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -364,39 +372,31 @@ class MethodDecoding extends Component {
|
|||||||
renderInputs () {
|
renderInputs () {
|
||||||
const { methodInputs } = this.state;
|
const { methodInputs } = this.state;
|
||||||
|
|
||||||
return methodInputs.map((input, index) => {
|
if (!methodInputs || methodInputs.length === 0) {
|
||||||
switch (input.type) {
|
return null;
|
||||||
case 'address':
|
|
||||||
return (
|
|
||||||
<InputAddress
|
|
||||||
disabled
|
|
||||||
text
|
|
||||||
key={ index }
|
|
||||||
className={ styles.input }
|
|
||||||
value={ input.value }
|
|
||||||
label={ input.type } />
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
readOnly
|
|
||||||
allowCopy
|
|
||||||
key={ index }
|
|
||||||
className={ styles.input }
|
|
||||||
value={ this.renderValue(input.value) }
|
|
||||||
label={ input.type } />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inputs = methodInputs.map((input, index) => {
|
||||||
|
return (
|
||||||
|
<TypedInput
|
||||||
|
allowCopy
|
||||||
|
className={ styles.input }
|
||||||
|
label={ input.type }
|
||||||
|
key={ index }
|
||||||
|
param={ input.type }
|
||||||
|
readOnly
|
||||||
|
value={ this.renderValue(input.value) }
|
||||||
|
/>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return inputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderValue (value) {
|
renderValue (value) {
|
||||||
const { api } = this.context;
|
const { api } = this.context;
|
||||||
|
|
||||||
if (api.util.isInstanceOf(value, BigNumber)) {
|
if (api.util.isArray(value)) {
|
||||||
return value.toFormat(0);
|
|
||||||
} else if (api.util.isArray(value)) {
|
|
||||||
return api.util.bytesToHex(value);
|
return api.util.bytesToHex(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,8 @@ import Contracts from '~/contracts';
|
|||||||
import Abi from '~/abi';
|
import Abi from '~/abi';
|
||||||
import * as abis from '~/contracts/abi';
|
import * as abis from '~/contracts/abi';
|
||||||
|
|
||||||
|
import { decodeMethodInput } from '~/api/util/decode';
|
||||||
|
|
||||||
const CONTRACT_CREATE = '0x60606040';
|
const CONTRACT_CREATE = '0x60606040';
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
@ -26,6 +28,8 @@ export default class MethodDecodingStore {
|
|||||||
|
|
||||||
api = null;
|
api = null;
|
||||||
|
|
||||||
|
_bytecodes = {};
|
||||||
|
_contractsAbi = {};
|
||||||
_isContract = {};
|
_isContract = {};
|
||||||
_methods = {};
|
_methods = {};
|
||||||
|
|
||||||
@ -46,12 +50,17 @@ export default class MethodDecodingStore {
|
|||||||
if (!contract || !contract.meta || !contract.meta.abi) {
|
if (!contract || !contract.meta || !contract.meta.abi) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loadFromAbi(contract.meta.abi);
|
this.loadFromAbi(contract.meta.abi, contract.address);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadFromAbi (_abi) {
|
loadFromAbi (_abi, contractAddress) {
|
||||||
const abi = new Abi(_abi);
|
const abi = new Abi(_abi);
|
||||||
|
|
||||||
|
if (contractAddress && abi) {
|
||||||
|
this._contractsAbi[contractAddress] = abi;
|
||||||
|
}
|
||||||
|
|
||||||
abi
|
abi
|
||||||
.functions
|
.functions
|
||||||
.map((f) => ({ sign: f.signature, abi: f.abi }))
|
.map((f) => ({ sign: f.signature, abi: f.abi }))
|
||||||
@ -111,6 +120,7 @@ export default class MethodDecodingStore {
|
|||||||
const contractAddress = isReceived ? transaction.from : transaction.to;
|
const contractAddress = isReceived ? transaction.from : transaction.to;
|
||||||
const input = transaction.input || transaction.data;
|
const input = transaction.input || transaction.data;
|
||||||
|
|
||||||
|
result.input = input;
|
||||||
result.received = isReceived;
|
result.received = isReceived;
|
||||||
|
|
||||||
// No input, should be a ETH transfer
|
// No input, should be a ETH transfer
|
||||||
@ -118,17 +128,20 @@ export default class MethodDecodingStore {
|
|||||||
return Promise.resolve(result);
|
return Promise.resolve(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
let signature;
|
||||||
const { signature } = this.api.util.decodeCallData(input);
|
|
||||||
|
|
||||||
if (signature === CONTRACT_CREATE || transaction.creates) {
|
try {
|
||||||
result.contract = true;
|
const decodeCallDataResult = this.api.util.decodeCallData(input);
|
||||||
return Promise.resolve({ ...result, deploy: true });
|
signature = decodeCallDataResult.signature;
|
||||||
}
|
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
|
// Contract deployment
|
||||||
|
if (!signature || signature === CONTRACT_CREATE || transaction.creates) {
|
||||||
|
return this.decodeContractCreation(result, contractAddress || transaction.creates);
|
||||||
|
}
|
||||||
|
|
||||||
return this
|
return this
|
||||||
.isContract(contractAddress || transaction.creates)
|
.isContract(contractAddress)
|
||||||
.then((isContract) => {
|
.then((isContract) => {
|
||||||
result.contract = isContract;
|
result.contract = isContract;
|
||||||
|
|
||||||
@ -140,11 +153,6 @@ export default class MethodDecodingStore {
|
|||||||
result.signature = signature;
|
result.signature = signature;
|
||||||
result.params = paramdata;
|
result.params = paramdata;
|
||||||
|
|
||||||
// Contract deployment
|
|
||||||
if (!signature) {
|
|
||||||
return Promise.resolve({ ...result, deploy: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
return this
|
return this
|
||||||
.fetchMethodAbi(signature)
|
.fetchMethodAbi(signature)
|
||||||
.then((abi) => {
|
.then((abi) => {
|
||||||
@ -173,6 +181,68 @@ export default class MethodDecodingStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
decodeContractCreation (data, contractAddress) {
|
||||||
|
const result = {
|
||||||
|
...data,
|
||||||
|
contract: true,
|
||||||
|
deploy: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const { input } = data;
|
||||||
|
const abi = this._contractsAbi[contractAddress];
|
||||||
|
|
||||||
|
if (!input || !abi || !abi.constructors || abi.constructors.length === 0) {
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const constructorAbi = abi.constructors[0];
|
||||||
|
|
||||||
|
const rawInput = /^(?:0x)?(.*)$/.exec(input)[1];
|
||||||
|
|
||||||
|
return this
|
||||||
|
.getCode(contractAddress)
|
||||||
|
.then((code) => {
|
||||||
|
if (!code || /^(0x)0*?$/.test(code)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawCode = /^(?:0x)?(.*)$/.exec(code)[1];
|
||||||
|
const codeOffset = rawInput.indexOf(rawCode);
|
||||||
|
|
||||||
|
if (codeOffset === -1) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params are the last bytes of the transaction Input
|
||||||
|
// (minus the bytecode). It seems that they are repeated
|
||||||
|
// twice
|
||||||
|
const params = rawInput.slice(codeOffset + rawCode.length);
|
||||||
|
const paramsBis = params.slice(params.length / 2);
|
||||||
|
|
||||||
|
let decodedInputs;
|
||||||
|
|
||||||
|
try {
|
||||||
|
decodedInputs = decodeMethodInput(constructorAbi, params);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!decodedInputs) {
|
||||||
|
decodedInputs = decodeMethodInput(constructorAbi, paramsBis);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
if (decodedInputs && decodedInputs.length > 0) {
|
||||||
|
result.inputs = decodedInputs
|
||||||
|
.map((value, index) => {
|
||||||
|
const type = constructorAbi.inputs[index].kind.type;
|
||||||
|
return { type, value };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fetchMethodAbi (signature) {
|
fetchMethodAbi (signature) {
|
||||||
if (this._methods[signature] !== undefined) {
|
if (this._methods[signature] !== undefined) {
|
||||||
return Promise.resolve(this._methods[signature]);
|
return Promise.resolve(this._methods[signature]);
|
||||||
@ -209,7 +279,7 @@ export default class MethodDecodingStore {
|
|||||||
return Promise.resolve(this._isContract[contractAddress]);
|
return Promise.resolve(this._isContract[contractAddress]);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._isContract[contractAddress] = this.api.eth
|
this._isContract[contractAddress] = this
|
||||||
.getCode(contractAddress)
|
.getCode(contractAddress)
|
||||||
.then((bytecode) => {
|
.then((bytecode) => {
|
||||||
// Is a contract if the address contains *valid* bytecode
|
// Is a contract if the address contains *valid* bytecode
|
||||||
@ -222,4 +292,24 @@ export default class MethodDecodingStore {
|
|||||||
return Promise.resolve(this._isContract[contractAddress]);
|
return Promise.resolve(this._isContract[contractAddress]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCode (contractAddress) {
|
||||||
|
// If zero address, resolve to '0x'
|
||||||
|
if (!contractAddress || /^(0x)?0*$/.test(contractAddress)) {
|
||||||
|
return Promise.resolve('0x');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._bytecodes[contractAddress]) {
|
||||||
|
return Promise.resolve(this._bytecodes[contractAddress]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._bytecodes[contractAddress] = this.api.eth
|
||||||
|
.getCode(contractAddress)
|
||||||
|
.then((bytecode) => {
|
||||||
|
this._bytecodes[contractAddress] = bytecode;
|
||||||
|
return this._bytecodes[contractAddress];
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve(this._bytecodes[contractAddress]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { LinearProgress } from 'material-ui';
|
import { LinearProgress } from 'material-ui';
|
||||||
@ -33,8 +34,8 @@ class TxHash extends Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
hash: PropTypes.string.isRequired,
|
hash: PropTypes.string.isRequired,
|
||||||
isTest: PropTypes.bool,
|
isTest: PropTypes.bool,
|
||||||
summary: PropTypes.bool,
|
maxConfirmations: PropTypes.number,
|
||||||
maxConfirmations: PropTypes.number
|
summary: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -43,14 +44,14 @@ class TxHash extends Component {
|
|||||||
|
|
||||||
state = {
|
state = {
|
||||||
blockNumber: new BigNumber(0),
|
blockNumber: new BigNumber(0),
|
||||||
transaction: null,
|
subscriptionId: null,
|
||||||
subscriptionId: null
|
transaction: null
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { api } = this.context;
|
const { api } = this.context;
|
||||||
|
|
||||||
api.subscribe('eth_blockNumber', this.onBlockNumber).then((subscriptionId) => {
|
return api.subscribe('eth_blockNumber', this.onBlockNumber).then((subscriptionId) => {
|
||||||
this.setState({ subscriptionId });
|
this.setState({ subscriptionId });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -59,28 +60,28 @@ class TxHash extends Component {
|
|||||||
const { api } = this.context;
|
const { api } = this.context;
|
||||||
const { subscriptionId } = this.state;
|
const { subscriptionId } = this.state;
|
||||||
|
|
||||||
api.unsubscribe(subscriptionId);
|
return api.unsubscribe(subscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { hash, isTest, summary } = this.props;
|
const { hash, isTest, summary } = this.props;
|
||||||
|
|
||||||
const link = (
|
const hashLink = (
|
||||||
<a href={ txLink(hash, isTest) } target='_blank'>
|
<a href={ txLink(hash, isTest) } target='_blank'>
|
||||||
<ShortenedHash data={ hash } />
|
<ShortenedHash data={ hash } />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
||||||
let header = (
|
|
||||||
<p>The transaction has been posted to the network, with a hash of { link }.</p>
|
|
||||||
);
|
|
||||||
if (summary) {
|
|
||||||
header = (<p>{ link }</p>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{ header }
|
<p>{
|
||||||
|
summary
|
||||||
|
? hashLink
|
||||||
|
: <FormattedMessage
|
||||||
|
id='ui.txHash.posted'
|
||||||
|
defaultMessage='The transaction has been posted to the network with a hash of {hashLink}'
|
||||||
|
values={ { hashLink } } />
|
||||||
|
}</p>
|
||||||
{ this.renderConfirmations() }
|
{ this.renderConfirmations() }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -98,20 +99,22 @@ class TxHash extends Component {
|
|||||||
color='white'
|
color='white'
|
||||||
mode='indeterminate'
|
mode='indeterminate'
|
||||||
/>
|
/>
|
||||||
<div className={ styles.progressinfo }>waiting for confirmations</div>
|
<div className={ styles.progressinfo }>
|
||||||
|
<FormattedMessage
|
||||||
|
id='ui.txHash.waiting'
|
||||||
|
defaultMessage='waiting for confirmations' />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmations = blockNumber.minus(transaction.blockNumber).plus(1);
|
const confirmations = blockNumber.minus(transaction.blockNumber).plus(1);
|
||||||
const value = Math.min(confirmations.toNumber(), maxConfirmations);
|
const value = Math.min(confirmations.toNumber(), maxConfirmations);
|
||||||
let count;
|
|
||||||
if (confirmations.gt(maxConfirmations)) {
|
let count = confirmations.toFormat(0);
|
||||||
count = confirmations.toFormat(0);
|
if (confirmations.lte(maxConfirmations)) {
|
||||||
} else {
|
count = `${count}/${maxConfirmations}`;
|
||||||
count = confirmations.toFormat(0) + `/${maxConfirmations}`;
|
|
||||||
}
|
}
|
||||||
const unit = value === 1 ? 'confirmation' : 'confirmations';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.confirm }>
|
<div className={ styles.confirm }>
|
||||||
@ -121,10 +124,17 @@ class TxHash extends Component {
|
|||||||
max={ maxConfirmations }
|
max={ maxConfirmations }
|
||||||
value={ value }
|
value={ value }
|
||||||
color='white'
|
color='white'
|
||||||
mode='determinate'
|
mode='determinate' />
|
||||||
/>
|
|
||||||
<div className={ styles.progressinfo }>
|
<div className={ styles.progressinfo }>
|
||||||
<abbr title={ `block #${blockNumber.toFormat(0)}` }>{ count } { unit }</abbr>
|
<abbr title={ `block #${blockNumber.toFormat(0)}` }>
|
||||||
|
<FormattedMessage
|
||||||
|
id='ui.txHash.confirmations'
|
||||||
|
defaultMessage='{count} {value, plural, one {confirmation} other {confirmations}}'
|
||||||
|
values={ {
|
||||||
|
count,
|
||||||
|
value
|
||||||
|
} } />
|
||||||
|
</abbr>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -138,15 +148,17 @@ class TxHash extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ blockNumber });
|
return api.eth
|
||||||
|
|
||||||
api.eth
|
|
||||||
.getTransactionReceipt(hash)
|
.getTransactionReceipt(hash)
|
||||||
.then((transaction) => {
|
.then((transaction) => {
|
||||||
this.setState({ transaction });
|
this.setState({
|
||||||
|
blockNumber,
|
||||||
|
transaction
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn('onBlockNumber', error);
|
console.warn('onBlockNumber', error);
|
||||||
|
this.setState({ blockNumber });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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() });
|
const api = new Api({ execute: sinon.stub() });
|
||||||
|
|
||||||
function renderShallow (props) {
|
function render (props) {
|
||||||
return shallow(
|
return shallow(
|
||||||
<TxRow
|
<TxRow
|
||||||
{ ...props } />,
|
{ ...props } />,
|
||||||
@ -33,7 +33,7 @@ function renderShallow (props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('ui/TxRow', () => {
|
describe('ui/TxList/TxRow', () => {
|
||||||
describe('rendering', () => {
|
describe('rendering', () => {
|
||||||
it('renders defaults', () => {
|
it('renders defaults', () => {
|
||||||
const block = {
|
const block = {
|
||||||
@ -45,7 +45,7 @@ describe('ui/TxRow', () => {
|
|||||||
value: new BigNumber(1)
|
value: new BigNumber(1)
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(renderShallow({ block, tx })).to.be.ok;
|
expect(render({ address: '0x123', block, isTest: true, tx })).to.be.ok;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -36,7 +36,7 @@ const STORE = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderShallow (props) {
|
function render (props) {
|
||||||
return shallow(
|
return shallow(
|
||||||
<TxList
|
<TxList
|
||||||
store={ STORE }
|
store={ STORE }
|
||||||
@ -48,7 +48,7 @@ function renderShallow (props) {
|
|||||||
describe('ui/TxList', () => {
|
describe('ui/TxList', () => {
|
||||||
describe('rendering', () => {
|
describe('rendering', () => {
|
||||||
it('renders defaults', () => {
|
it('renders defaults', () => {
|
||||||
expect(renderShallow()).to.be.ok;
|
expect(render({ address: '0x123', hashes: [] })).to.be.ok;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags } from '~/ui';
|
import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags } from '~/ui';
|
||||||
import CopyToClipboard from '~/ui/CopyToClipboard';
|
import CopyToClipboard from '~/ui/CopyToClipboard';
|
||||||
@ -26,50 +27,45 @@ export default class Header extends Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: PropTypes.object,
|
account: PropTypes.object,
|
||||||
balance: PropTypes.object,
|
balance: PropTypes.object,
|
||||||
className: PropTypes.string,
|
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
isContract: PropTypes.bool,
|
className: PropTypes.string,
|
||||||
hideName: PropTypes.bool
|
hideName: PropTypes.bool,
|
||||||
|
isContract: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
className: '',
|
|
||||||
children: null,
|
children: null,
|
||||||
isContract: false,
|
className: '',
|
||||||
hideName: false
|
hideName: false,
|
||||||
|
isContract: false
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, balance, className, children, hideName } = this.props;
|
const { account, balance, children, className, hideName } = this.props;
|
||||||
const { address, meta, uuid } = account;
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uuidText = !uuid
|
const { address } = account;
|
||||||
? null
|
const meta = account.meta || {};
|
||||||
: <div className={ styles.uuidline }>uuid: { uuid }</div>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ className }>
|
<div className={ className }>
|
||||||
<Container>
|
<Container>
|
||||||
<IdentityIcon
|
<IdentityIcon address={ address } />
|
||||||
address={ address } />
|
|
||||||
<div className={ styles.floatleft }>
|
<div className={ styles.floatleft }>
|
||||||
{ this.renderName(address) }
|
{ this.renderName() }
|
||||||
|
|
||||||
<div className={ [ hideName ? styles.bigaddress : '', styles.addressline ].join(' ') }>
|
<div className={ [ hideName ? styles.bigaddress : '', styles.addressline ].join(' ') }>
|
||||||
<CopyToClipboard data={ address } />
|
<CopyToClipboard data={ address } />
|
||||||
<div className={ styles.address }>{ address }</div>
|
<div className={ styles.address }>{ address }</div>
|
||||||
</div>
|
</div>
|
||||||
|
{ this.renderUuid() }
|
||||||
{ uuidText }
|
|
||||||
<div className={ styles.infoline }>
|
<div className={ styles.infoline }>
|
||||||
{ meta.description }
|
{ meta.description }
|
||||||
</div>
|
</div>
|
||||||
{ this.renderTxCount() }
|
{ this.renderTxCount() }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={ styles.tags }>
|
<div className={ styles.tags }>
|
||||||
<Tags tags={ meta.tags } />
|
<Tags tags={ meta.tags } />
|
||||||
</div>
|
</div>
|
||||||
@ -77,9 +73,7 @@ export default class Header extends Component {
|
|||||||
<Balance
|
<Balance
|
||||||
account={ account }
|
account={ account }
|
||||||
balance={ balance } />
|
balance={ balance } />
|
||||||
<Certifications
|
<Certifications address={ address } />
|
||||||
account={ account.address }
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{ children }
|
{ children }
|
||||||
</Container>
|
</Container>
|
||||||
@ -87,15 +81,22 @@ export default class Header extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderName (address) {
|
renderName () {
|
||||||
const { hideName } = this.props;
|
const { hideName } = this.props;
|
||||||
|
|
||||||
if (hideName) {
|
if (hideName) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { address } = this.props.account;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContainerTitle title={ <IdentityName address={ address } unknown /> } />
|
<ContainerTitle
|
||||||
|
title={
|
||||||
|
<IdentityName
|
||||||
|
address={ address }
|
||||||
|
unknown />
|
||||||
|
} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +115,31 @@ export default class Header extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.infoline }>
|
<div className={ styles.infoline }>
|
||||||
{ txCount.toFormat() } outgoing transactions
|
<FormattedMessage
|
||||||
|
id='account.header.outgoingTransactions'
|
||||||
|
defaultMessage='{count} outgoing transactions'
|
||||||
|
values={ {
|
||||||
|
count: txCount.toFormat()
|
||||||
|
} } />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderUuid () {
|
||||||
|
const { uuid } = this.props.account;
|
||||||
|
|
||||||
|
if (!uuid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={ styles.uuidline }>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.header.uuid'
|
||||||
|
defaultMessage='uuid: {uuid}'
|
||||||
|
values={ {
|
||||||
|
uuid
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
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
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
import etherscan from '~/3rdparty/etherscan';
|
|
||||||
import { Container, TxList, Loading } from '~/ui';
|
import { Container, TxList, Loading } from '~/ui';
|
||||||
|
|
||||||
|
import Store from './store';
|
||||||
import styles from './transactions.css';
|
import styles from './transactions.css';
|
||||||
|
|
||||||
|
@observer
|
||||||
class Transactions extends Component {
|
class Transactions extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object.isRequired
|
api: PropTypes.object.isRequired
|
||||||
@ -34,34 +37,35 @@ class Transactions extends Component {
|
|||||||
traceMode: PropTypes.bool
|
traceMode: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
store = new Store(this.context.api);
|
||||||
hashes: [],
|
|
||||||
loading: true,
|
|
||||||
callInfo: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
componentWillMount () {
|
||||||
this.getTransactions(this.props);
|
this.store.updateProps(this.props);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (newProps) {
|
componentWillReceiveProps (newProps) {
|
||||||
if (this.props.traceMode === undefined && newProps.traceMode !== undefined) {
|
if (this.props.traceMode === undefined && newProps.traceMode !== undefined) {
|
||||||
this.getTransactions(newProps);
|
this.store.updateProps(newProps);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasChanged = [ 'isTest', 'address' ]
|
const hasChanged = ['isTest', 'address']
|
||||||
.map(key => newProps[key] !== this.props[key])
|
.map(key => newProps[key] !== this.props[key])
|
||||||
.reduce((truth, keyTruth) => truth || keyTruth, false);
|
.reduce((truth, keyTruth) => truth || keyTruth, false);
|
||||||
|
|
||||||
if (hasChanged) {
|
if (hasChanged) {
|
||||||
this.getTransactions(newProps);
|
this.store.updateProps(newProps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<Container title='transactions'>
|
<Container
|
||||||
|
title={
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.transactions.title'
|
||||||
|
defaultMessage='transactions' />
|
||||||
|
}>
|
||||||
{ this.renderTransactionList() }
|
{ this.renderTransactionList() }
|
||||||
{ this.renderEtherscanFooter() }
|
{ this.renderEtherscanFooter() }
|
||||||
</Container>
|
</Container>
|
||||||
@ -69,10 +73,9 @@ class Transactions extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderTransactionList () {
|
renderTransactionList () {
|
||||||
const { address } = this.props;
|
const { address, isLoading, txHashes } = this.store;
|
||||||
const { hashes, loading } = this.state;
|
|
||||||
|
|
||||||
if (loading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Loading />
|
<Loading />
|
||||||
);
|
);
|
||||||
@ -81,85 +84,29 @@ class Transactions extends Component {
|
|||||||
return (
|
return (
|
||||||
<TxList
|
<TxList
|
||||||
address={ address }
|
address={ address }
|
||||||
hashes={ hashes }
|
hashes={ txHashes }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEtherscanFooter () {
|
renderEtherscanFooter () {
|
||||||
const { traceMode } = this.props;
|
const { isTracing } = this.store;
|
||||||
|
|
||||||
if (traceMode) {
|
if (isTracing) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.etherscan }>
|
<div className={ styles.etherscan }>
|
||||||
Transaction list powered by <a href='https://etherscan.io/' target='_blank'>etherscan.io</a>
|
<FormattedMessage
|
||||||
|
id='account.transactions.poweredBy'
|
||||||
|
defaultMessage='Transaction list powered by {etherscan}'
|
||||||
|
values={ {
|
||||||
|
etherscan: <a href='https://etherscan.io/' target='_blank'>etherscan.io</a>
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTransactions = (props) => {
|
|
||||||
const { isTest, address, traceMode } = props;
|
|
||||||
|
|
||||||
// Don't fetch the transactions if we don't know in which
|
|
||||||
// network we are yet...
|
|
||||||
if (isTest === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this
|
|
||||||
.fetchTransactions(isTest, address, traceMode)
|
|
||||||
.then((transactions) => {
|
|
||||||
this.setState({
|
|
||||||
hashes: transactions.map((transaction) => transaction.hash),
|
|
||||||
loading: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchTransactions = (isTest, address, traceMode) => {
|
|
||||||
// if (traceMode) {
|
|
||||||
// return this.fetchTraceTransactions(address);
|
|
||||||
// }
|
|
||||||
|
|
||||||
return this.fetchEtherscanTransactions(isTest, address);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchEtherscanTransactions = (isTest, address) => {
|
|
||||||
return etherscan.account
|
|
||||||
.transactions(address, 0, isTest)
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('getTransactions', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchTraceTransactions = (address) => {
|
|
||||||
return Promise
|
|
||||||
.all([
|
|
||||||
this.context.api.trace
|
|
||||||
.filter({
|
|
||||||
fromBlock: 0,
|
|
||||||
fromAddress: address
|
|
||||||
}),
|
|
||||||
this.context.api.trace
|
|
||||||
.filter({
|
|
||||||
fromBlock: 0,
|
|
||||||
toAddress: address
|
|
||||||
})
|
|
||||||
])
|
|
||||||
.then(([fromTransactions, toTransactions]) => {
|
|
||||||
const transactions = [].concat(fromTransactions, toTransactions);
|
|
||||||
|
|
||||||
return transactions.map(transaction => ({
|
|
||||||
from: transaction.action.from,
|
|
||||||
to: transaction.action.to,
|
|
||||||
blockNumber: transaction.blockNumber,
|
|
||||||
hash: transaction.transactionHash
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps (state) {
|
function mapStateToProps (state) {
|
||||||
|
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
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import ActionDelete from 'material-ui/svg-icons/action/delete';
|
|
||||||
import ContentCreate from 'material-ui/svg-icons/content/create';
|
|
||||||
import ContentSend from 'material-ui/svg-icons/content/send';
|
|
||||||
import LockIcon from 'material-ui/svg-icons/action/lock';
|
|
||||||
import VerifyIcon from 'material-ui/svg-icons/action/verified-user';
|
|
||||||
|
|
||||||
import { EditMeta, DeleteAccount, Shapeshift, Verification, Transfer, PasswordManager } from '~/modals';
|
|
||||||
import { Actionbar, Button, Page } from '~/ui';
|
|
||||||
|
|
||||||
import shapeshiftBtn from '~/../assets/images/shapeshift-btn.png';
|
import shapeshiftBtn from '~/../assets/images/shapeshift-btn.png';
|
||||||
|
import { EditMeta, DeleteAccount, Shapeshift, Verification, Transfer, PasswordManager } from '~/modals';
|
||||||
import Header from './Header';
|
|
||||||
import Transactions from './Transactions';
|
|
||||||
import { setVisibleAccounts } from '~/redux/providers/personalActions';
|
import { setVisibleAccounts } from '~/redux/providers/personalActions';
|
||||||
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
|
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
|
||||||
|
import { Actionbar, Button, Page } from '~/ui';
|
||||||
|
import { DeleteIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon } from '~/ui/Icons';
|
||||||
|
|
||||||
|
import Header from './Header';
|
||||||
|
import Store from './store';
|
||||||
|
import Transactions from './Transactions';
|
||||||
import styles from './account.css';
|
import styles from './account.css';
|
||||||
|
|
||||||
|
@observer
|
||||||
class Account extends Component {
|
class Account extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
setVisibleAccounts: PropTypes.func.isRequired,
|
|
||||||
fetchCertifiers: PropTypes.func.isRequired,
|
fetchCertifiers: PropTypes.func.isRequired,
|
||||||
fetchCertifications: PropTypes.func.isRequired,
|
fetchCertifications: PropTypes.func.isRequired,
|
||||||
images: PropTypes.object.isRequired,
|
images: PropTypes.object.isRequired,
|
||||||
|
setVisibleAccounts: PropTypes.func.isRequired,
|
||||||
|
|
||||||
params: PropTypes.object,
|
|
||||||
accounts: PropTypes.object,
|
accounts: PropTypes.object,
|
||||||
balances: PropTypes.object
|
balances: PropTypes.object,
|
||||||
|
params: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
store = new Store();
|
||||||
showDeleteDialog: false,
|
|
||||||
showEditDialog: false,
|
|
||||||
showFundDialog: false,
|
|
||||||
showVerificationDialog: false,
|
|
||||||
showTransferDialog: false,
|
|
||||||
showPasswordDialog: false
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.props.fetchCertifiers();
|
this.props.fetchCertifiers();
|
||||||
@ -76,7 +67,8 @@ class Account extends Component {
|
|||||||
|
|
||||||
setVisibleAccounts (props = this.props) {
|
setVisibleAccounts (props = this.props) {
|
||||||
const { params, setVisibleAccounts, fetchCertifications } = props;
|
const { params, setVisibleAccounts, fetchCertifications } = props;
|
||||||
const addresses = [ params.address ];
|
const addresses = [params.address];
|
||||||
|
|
||||||
setVisibleAccounts(addresses);
|
setVisibleAccounts(addresses);
|
||||||
fetchCertifications(params.address);
|
fetchCertifications(params.address);
|
||||||
}
|
}
|
||||||
@ -97,15 +89,14 @@ class Account extends Component {
|
|||||||
{ this.renderDeleteDialog(account) }
|
{ this.renderDeleteDialog(account) }
|
||||||
{ this.renderEditDialog(account) }
|
{ this.renderEditDialog(account) }
|
||||||
{ this.renderFundDialog() }
|
{ this.renderFundDialog() }
|
||||||
|
{ this.renderPasswordDialog(account) }
|
||||||
|
{ this.renderTransferDialog(account, balance) }
|
||||||
{ this.renderVerificationDialog() }
|
{ this.renderVerificationDialog() }
|
||||||
{ this.renderTransferDialog() }
|
{ this.renderActionbar(balance) }
|
||||||
{ this.renderPasswordDialog() }
|
|
||||||
{ this.renderActionbar() }
|
|
||||||
<Page>
|
<Page>
|
||||||
<Header
|
<Header
|
||||||
account={ account }
|
account={ account }
|
||||||
balance={ balance }
|
balance={ balance } />
|
||||||
/>
|
|
||||||
<Transactions
|
<Transactions
|
||||||
accounts={ accounts }
|
accounts={ accounts }
|
||||||
address={ address } />
|
address={ address } />
|
||||||
@ -114,86 +105,108 @@ class Account extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderActionbar () {
|
renderActionbar (balance) {
|
||||||
const { address } = this.props.params;
|
|
||||||
const { balances } = this.props;
|
|
||||||
const balance = balances[address];
|
|
||||||
|
|
||||||
const showTransferButton = !!(balance && balance.tokens);
|
const showTransferButton = !!(balance && balance.tokens);
|
||||||
|
|
||||||
const buttons = [
|
const buttons = [
|
||||||
<Button
|
<Button
|
||||||
key='transferFunds'
|
|
||||||
icon={ <ContentSend /> }
|
|
||||||
label='transfer'
|
|
||||||
disabled={ !showTransferButton }
|
disabled={ !showTransferButton }
|
||||||
onClick={ this.onTransferClick } />,
|
icon={ <SendIcon /> }
|
||||||
|
key='transferFunds'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.button.transfer'
|
||||||
|
defaultMessage='transfer' />
|
||||||
|
}
|
||||||
|
onClick={ this.store.toggleTransferDialog } />,
|
||||||
<Button
|
<Button
|
||||||
|
icon={
|
||||||
|
<img
|
||||||
|
className={ styles.btnicon }
|
||||||
|
src={ shapeshiftBtn } />
|
||||||
|
}
|
||||||
key='shapeshift'
|
key='shapeshift'
|
||||||
icon={ <img src={ shapeshiftBtn } className={ styles.btnicon } /> }
|
label={
|
||||||
label='shapeshift'
|
<FormattedMessage
|
||||||
onClick={ this.onShapeshiftAccountClick } />,
|
id='account.button.shapeshift'
|
||||||
|
defaultMessage='shapeshift' />
|
||||||
|
}
|
||||||
|
onClick={ this.store.toggleFundDialog } />,
|
||||||
<Button
|
<Button
|
||||||
key='sms-verification'
|
|
||||||
icon={ <VerifyIcon /> }
|
icon={ <VerifyIcon /> }
|
||||||
label='Verify'
|
key='sms-verification'
|
||||||
onClick={ this.openVerification } />,
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.button.verify'
|
||||||
|
defaultMessage='verify' />
|
||||||
|
}
|
||||||
|
onClick={ this.store.toggleVerificationDialog } />,
|
||||||
<Button
|
<Button
|
||||||
|
icon={ <EditIcon /> }
|
||||||
key='editmeta'
|
key='editmeta'
|
||||||
icon={ <ContentCreate /> }
|
label={
|
||||||
label='edit'
|
<FormattedMessage
|
||||||
onClick={ this.onEditClick } />,
|
id='account.button.edit'
|
||||||
|
defaultMessage='edit' />
|
||||||
|
}
|
||||||
|
onClick={ this.store.toggleEditDialog } />,
|
||||||
<Button
|
<Button
|
||||||
|
icon={ <LockedIcon /> }
|
||||||
key='passwordManager'
|
key='passwordManager'
|
||||||
icon={ <LockIcon /> }
|
label={
|
||||||
label='password'
|
<FormattedMessage
|
||||||
onClick={ this.onPasswordClick } />,
|
id='account.button.password'
|
||||||
|
defaultMessage='password' />
|
||||||
|
}
|
||||||
|
onClick={ this.store.togglePasswordDialog } />,
|
||||||
<Button
|
<Button
|
||||||
|
icon={ <DeleteIcon /> }
|
||||||
key='delete'
|
key='delete'
|
||||||
icon={ <ActionDelete /> }
|
label={
|
||||||
label='delete account'
|
<FormattedMessage
|
||||||
onClick={ this.onDeleteClick } />
|
id='account.button.delete'
|
||||||
|
defaultMessage='delete account' />
|
||||||
|
}
|
||||||
|
onClick={ this.store.toggleDeleteDialog } />
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Actionbar
|
<Actionbar
|
||||||
title='Account Management'
|
buttons={ buttons }
|
||||||
buttons={ buttons } />
|
title={
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.title'
|
||||||
|
defaultMessage='Account Management' />
|
||||||
|
} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDeleteDialog (account) {
|
renderDeleteDialog (account) {
|
||||||
const { showDeleteDialog } = this.state;
|
if (!this.store.isDeleteVisible) {
|
||||||
|
|
||||||
if (!showDeleteDialog) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DeleteAccount
|
<DeleteAccount
|
||||||
account={ account }
|
account={ account }
|
||||||
onClose={ this.onDeleteClose } />
|
onClose={ this.store.toggleDeleteDialog } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEditDialog (account) {
|
renderEditDialog (account) {
|
||||||
const { showEditDialog } = this.state;
|
if (!this.store.isEditVisible) {
|
||||||
|
|
||||||
if (!showEditDialog) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditMeta
|
<EditMeta
|
||||||
account={ account }
|
account={ account }
|
||||||
onClose={ this.onEditClick } />
|
onClose={ this.store.toggleEditDialog } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFundDialog () {
|
renderFundDialog () {
|
||||||
const { showFundDialog } = this.state;
|
if (!this.store.isFundVisible) {
|
||||||
|
|
||||||
if (!showFundDialog) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,12 +215,41 @@ class Account extends Component {
|
|||||||
return (
|
return (
|
||||||
<Shapeshift
|
<Shapeshift
|
||||||
address={ address }
|
address={ address }
|
||||||
onClose={ this.onShapeshiftAccountClose } />
|
onClose={ this.store.toggleFundDialog } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPasswordDialog (account) {
|
||||||
|
if (!this.store.isPasswordVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PasswordManager
|
||||||
|
account={ account }
|
||||||
|
onClose={ this.store.togglePasswordDialog } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTransferDialog (account, balance) {
|
||||||
|
if (!this.store.isTransferVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { balances, images } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transfer
|
||||||
|
account={ account }
|
||||||
|
balance={ balance }
|
||||||
|
balances={ balances }
|
||||||
|
images={ images }
|
||||||
|
onClose={ this.store.toggleTransferDialog } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderVerificationDialog () {
|
renderVerificationDialog () {
|
||||||
if (!this.state.showVerificationDialog) {
|
if (!this.store.isVerificationVisible) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,102 +258,9 @@ class Account extends Component {
|
|||||||
return (
|
return (
|
||||||
<Verification
|
<Verification
|
||||||
account={ address }
|
account={ address }
|
||||||
onClose={ this.onVerificationClose }
|
onClose={ this.store.toggleVerificationDialog } />
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTransferDialog () {
|
|
||||||
const { showTransferDialog } = this.state;
|
|
||||||
|
|
||||||
if (!showTransferDialog) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { address } = this.props.params;
|
|
||||||
const { accounts, balances, images } = this.props;
|
|
||||||
const account = accounts[address];
|
|
||||||
const balance = balances[address];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transfer
|
|
||||||
account={ account }
|
|
||||||
balance={ balance }
|
|
||||||
balances={ balances }
|
|
||||||
images={ images }
|
|
||||||
onClose={ this.onTransferClose } />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPasswordDialog () {
|
|
||||||
const { showPasswordDialog } = this.state;
|
|
||||||
|
|
||||||
if (!showPasswordDialog) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { address } = this.props.params;
|
|
||||||
const { accounts } = this.props;
|
|
||||||
const account = accounts[address];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PasswordManager
|
|
||||||
account={ account }
|
|
||||||
onClose={ this.onPasswordClose } />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onDeleteClick = () => {
|
|
||||||
this.setState({ showDeleteDialog: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
onDeleteClose = () => {
|
|
||||||
this.setState({ showDeleteDialog: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditClick = () => {
|
|
||||||
this.setState({
|
|
||||||
showEditDialog: !this.state.showEditDialog
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onShapeshiftAccountClick = () => {
|
|
||||||
this.setState({
|
|
||||||
showFundDialog: !this.state.showFundDialog
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onShapeshiftAccountClose = () => {
|
|
||||||
this.onShapeshiftAccountClick();
|
|
||||||
}
|
|
||||||
|
|
||||||
openVerification = () => {
|
|
||||||
this.setState({ showVerificationDialog: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
onVerificationClose = () => {
|
|
||||||
this.setState({ showVerificationDialog: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
onTransferClick = () => {
|
|
||||||
this.setState({
|
|
||||||
showTransferDialog: !this.state.showTransferDialog
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onTransferClose = () => {
|
|
||||||
this.onTransferClick();
|
|
||||||
}
|
|
||||||
|
|
||||||
onPasswordClick = () => {
|
|
||||||
this.setState({
|
|
||||||
showPasswordDialog: !this.state.showPasswordDialog
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onPasswordClose = () => {
|
|
||||||
this.onPasswordClick();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps (state) {
|
function mapStateToProps (state) {
|
||||||
@ -328,9 +277,9 @@ function mapStateToProps (state) {
|
|||||||
|
|
||||||
function mapDispatchToProps (dispatch) {
|
function mapDispatchToProps (dispatch) {
|
||||||
return bindActionCreators({
|
return bindActionCreators({
|
||||||
setVisibleAccounts,
|
|
||||||
fetchCertifiers,
|
fetchCertifiers,
|
||||||
fetchCertifications
|
fetchCertifications,
|
||||||
|
setVisibleAccounts
|
||||||
}, dispatch);
|
}, dispatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<Certifications account={ account.address } />
|
<Certifications address={ account.address } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,13 +17,9 @@
|
|||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
|
||||||
import ActionCompareArrows from 'material-ui/svg-icons/action/compare-arrows';
|
|
||||||
import ActionDashboard from 'material-ui/svg-icons/action/dashboard';
|
|
||||||
import HardwareDesktopMac from 'material-ui/svg-icons/hardware/desktop-mac';
|
|
||||||
import NotificationVpnLock from 'material-ui/svg-icons/notification/vpn-lock';
|
|
||||||
|
|
||||||
import { Input } from '~/ui';
|
import { Input } from '~/ui';
|
||||||
|
import { CompareIcon, ComputerIcon, DashboardIcon, VpnIcon } from '~/ui/Icons';
|
||||||
|
|
||||||
import styles from './connection.css';
|
import styles from './connection.css';
|
||||||
|
|
||||||
@ -51,13 +47,6 @@ class Connection extends Component {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeIcon = needsToken
|
|
||||||
? <NotificationVpnLock className={ styles.svg } />
|
|
||||||
: <ActionDashboard className={ styles.svg } />;
|
|
||||||
const description = needsToken
|
|
||||||
? this.renderSigner()
|
|
||||||
: this.renderPing();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={ styles.overlay } />
|
<div className={ styles.overlay } />
|
||||||
@ -65,16 +54,24 @@ class Connection extends Component {
|
|||||||
<div className={ styles.body }>
|
<div className={ styles.body }>
|
||||||
<div className={ styles.icons }>
|
<div className={ styles.icons }>
|
||||||
<div className={ styles.icon }>
|
<div className={ styles.icon }>
|
||||||
<HardwareDesktopMac className={ styles.svg } />
|
<ComputerIcon className={ styles.svg } />
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.iconSmall }>
|
<div className={ styles.iconSmall }>
|
||||||
<ActionCompareArrows className={ `${styles.svg} ${styles.pulse}` } />
|
<CompareIcon className={ `${styles.svg} ${styles.pulse}` } />
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.icon }>
|
<div className={ styles.icon }>
|
||||||
{ typeIcon }
|
{
|
||||||
|
needsToken
|
||||||
|
? <VpnIcon className={ styles.svg } />
|
||||||
|
: <DashboardIcon className={ styles.svg } />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ description }
|
{
|
||||||
|
needsToken
|
||||||
|
? this.renderSigner()
|
||||||
|
: this.renderPing()
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -144,10 +141,19 @@ class Connection extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeToken = (event, _token) => {
|
validateToken = (_token) => {
|
||||||
const token = _token.trim();
|
const token = _token.trim();
|
||||||
const validToken = /^[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}$/.test(token);
|
const validToken = /^[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}(-)?[a-zA-Z0-9]{4}$/.test(token);
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
validToken
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeToken = (event, _token) => {
|
||||||
|
const { token, validToken } = this.validateToken(_token || event.target.value);
|
||||||
|
|
||||||
this.setState({ token, validToken }, () => {
|
this.setState({ token, validToken }, () => {
|
||||||
validToken && this.setToken();
|
validToken && this.setToken();
|
||||||
});
|
});
|
||||||
@ -159,7 +165,7 @@ class Connection extends Component {
|
|||||||
|
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
|
|
||||||
api
|
return api
|
||||||
.updateToken(token, 0)
|
.updateToken(token, 0)
|
||||||
.then((isValid) => {
|
.then((isValid) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -173,14 +179,14 @@ class Connection extends Component {
|
|||||||
function mapStateToProps (state) {
|
function mapStateToProps (state) {
|
||||||
const { isConnected, isConnecting, needsToken } = state.nodeStatus;
|
const { isConnected, isConnecting, needsToken } = state.nodeStatus;
|
||||||
|
|
||||||
return { isConnected, isConnecting, needsToken };
|
return {
|
||||||
}
|
isConnected,
|
||||||
|
isConnecting,
|
||||||
function mapDispatchToProps (dispatch) {
|
needsToken
|
||||||
return bindActionCreators({}, dispatch);
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
null
|
||||||
)(Connection);
|
)(Connection);
|
||||||
|
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%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.full {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: white;
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 300;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
text-align: center;
|
||||||
|
padding: 5em;
|
||||||
|
font-size: 2em;
|
||||||
|
color: #999;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import DappsStore from '../Dapps/dappsStore';
|
import DappsStore from '../Dapps/dappsStore';
|
||||||
|
|
||||||
@ -25,21 +26,71 @@ import styles from './dapp.css';
|
|||||||
export default class Dapp extends Component {
|
export default class Dapp extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object.isRequired
|
api: PropTypes.object.isRequired
|
||||||
}
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
params: PropTypes.object
|
params: PropTypes.object
|
||||||
};
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
app: null,
|
||||||
|
loading: true
|
||||||
|
};
|
||||||
|
|
||||||
store = DappsStore.get(this.context.api);
|
store = DappsStore.get(this.context.api);
|
||||||
|
|
||||||
|
componentWillMount () {
|
||||||
|
const { id } = this.props.params;
|
||||||
|
this.loadApp(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (nextProps.params.id !== this.props.params.id) {
|
||||||
|
this.loadApp(nextProps.params.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadApp (id) {
|
||||||
|
this.setState({ loading: true });
|
||||||
|
|
||||||
|
this.store
|
||||||
|
.loadApp(id)
|
||||||
|
.then((app) => {
|
||||||
|
this.setState({ loading: false, app });
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.setState({ loading: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { dappsUrl } = this.context.api;
|
const { dappsUrl } = this.context.api;
|
||||||
const { id } = this.props.params;
|
const { app, loading } = this.state;
|
||||||
const app = this.store.apps.find((app) => app.id === id);
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className={ styles.full }>
|
||||||
|
<div className={ styles.text }>
|
||||||
|
<FormattedMessage
|
||||||
|
id='dapp.loading'
|
||||||
|
defaultMessage='Loading'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!app) {
|
if (!app) {
|
||||||
return null;
|
return (
|
||||||
|
<div className={ styles.full }>
|
||||||
|
<div className={ styles.text }>
|
||||||
|
<FormattedMessage
|
||||||
|
id='dapp.unavailable'
|
||||||
|
defaultMessage='The dapp cannot be reached'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let src = null;
|
let src = null;
|
||||||
|
@ -45,6 +45,10 @@ class Dapps extends Component {
|
|||||||
store = DappsStore.get(this.context.api);
|
store = DappsStore.get(this.context.api);
|
||||||
permissionStore = new PermissionStore(this.context.api);
|
permissionStore = new PermissionStore(this.context.api);
|
||||||
|
|
||||||
|
componentWillMount () {
|
||||||
|
this.store.loadAllApps();
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let externalOverlay = null;
|
let externalOverlay = null;
|
||||||
if (this.store.externalOverlayVisible) {
|
if (this.store.externalOverlayVisible) {
|
||||||
|
@ -48,17 +48,43 @@ export default class DappsStore {
|
|||||||
|
|
||||||
this.readDisplayApps();
|
this.readDisplayApps();
|
||||||
this.loadExternalOverlay();
|
this.loadExternalOverlay();
|
||||||
this.loadApps();
|
|
||||||
this.subscribeToChanges();
|
this.subscribeToChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadApps () {
|
/**
|
||||||
|
* Try to find the app from the local (local or builtin)
|
||||||
|
* apps, else fetch from the node
|
||||||
|
*/
|
||||||
|
loadApp (id) {
|
||||||
const { dappReg } = Contracts.get();
|
const { dappReg } = Contracts.get();
|
||||||
|
|
||||||
Promise
|
return this
|
||||||
|
.loadLocalApps()
|
||||||
|
.then(() => {
|
||||||
|
const app = this.apps.find((app) => app.id === id);
|
||||||
|
|
||||||
|
if (app) {
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fetchRegistryApp(dappReg, id, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadLocalApps () {
|
||||||
|
return Promise
|
||||||
.all([
|
.all([
|
||||||
this.fetchBuiltinApps().then((apps) => this.addApps(apps)),
|
this.fetchBuiltinApps().then((apps) => this.addApps(apps)),
|
||||||
this.fetchLocalApps().then((apps) => this.addApps(apps)),
|
this.fetchLocalApps().then((apps) => this.addApps(apps))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAllApps () {
|
||||||
|
const { dappReg } = Contracts.get();
|
||||||
|
|
||||||
|
return Promise
|
||||||
|
.all([
|
||||||
|
this.loadLocalApps(),
|
||||||
this.fetchRegistryApps(dappReg).then((apps) => this.addApps(apps))
|
this.fetchRegistryApps(dappReg).then((apps) => this.addApps(apps))
|
||||||
])
|
])
|
||||||
.then(this.writeDisplayApps);
|
.then(this.writeDisplayApps);
|
||||||
@ -67,8 +93,6 @@ export default class DappsStore {
|
|||||||
static get (api) {
|
static get (api) {
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
instance = new DappsStore(api);
|
instance = new DappsStore(api);
|
||||||
} else {
|
|
||||||
instance.loadApps();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
@ -20,6 +20,7 @@ import { connect } from 'react-redux';
|
|||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import RaisedButton from 'material-ui/RaisedButton';
|
import RaisedButton from 'material-ui/RaisedButton';
|
||||||
import ReactTooltip from 'react-tooltip';
|
import ReactTooltip from 'react-tooltip';
|
||||||
|
import keycode from 'keycode';
|
||||||
|
|
||||||
import { Form, Input, IdentityIcon } from '~/ui';
|
import { Form, Input, IdentityIcon } from '~/ui';
|
||||||
|
|
||||||
@ -207,7 +208,9 @@ class TransactionPendingFormConfirm extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown = (event) => {
|
onKeyDown = (event) => {
|
||||||
if (event.which !== 13) {
|
const codeName = keycode(event);
|
||||||
|
|
||||||
|
if (codeName !== 'enter') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ class Wallet extends Component {
|
|||||||
owned: PropTypes.bool.isRequired,
|
owned: PropTypes.bool.isRequired,
|
||||||
setVisibleAccounts: PropTypes.func.isRequired,
|
setVisibleAccounts: PropTypes.func.isRequired,
|
||||||
wallet: PropTypes.object.isRequired,
|
wallet: PropTypes.object.isRequired,
|
||||||
walletAccount: nullableProptype(PropTypes.object).isRequired
|
walletAccount: nullableProptype(PropTypes.object.isRequired)
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -180,7 +180,7 @@ class Wallet extends Component {
|
|||||||
const { address, isTest, wallet } = this.props;
|
const { address, isTest, wallet } = this.props;
|
||||||
const { owners, require, confirmations, transactions } = wallet;
|
const { owners, require, confirmations, transactions } = wallet;
|
||||||
|
|
||||||
if (!isTest || !owners || !require) {
|
if (!owners || !require) {
|
||||||
return (
|
return (
|
||||||
<div style={ { marginTop: '4em' } }>
|
<div style={ { marginTop: '4em' } }>
|
||||||
<Loading size={ 4 } />
|
<Loading size={ 4 } />
|
||||||
|
@ -26,6 +26,7 @@ injectTapEventPlugin();
|
|||||||
import chai from 'chai';
|
import chai from 'chai';
|
||||||
import chaiAsPromised from 'chai-as-promised';
|
import chaiAsPromised from 'chai-as-promised';
|
||||||
import chaiEnzyme from 'chai-enzyme';
|
import chaiEnzyme from 'chai-enzyme';
|
||||||
|
import 'sinon-as-promised';
|
||||||
import sinonChai from 'sinon-chai';
|
import sinonChai from 'sinon-chai';
|
||||||
import { WebSocket } from 'mock-socket';
|
import { WebSocket } from 'mock-socket';
|
||||||
import jsdom from 'jsdom';
|
import jsdom from 'jsdom';
|
||||||
|
@ -59,9 +59,9 @@ impl BlockChain {
|
|||||||
mix_hash: self.genesis_block.mix_hash.clone(),
|
mix_hash: self.genesis_block.mix_hash.clone(),
|
||||||
}),
|
}),
|
||||||
difficulty: self.genesis_block.difficulty,
|
difficulty: self.genesis_block.difficulty,
|
||||||
author: self.genesis_block.author.clone(),
|
author: Some(self.genesis_block.author.clone()),
|
||||||
timestamp: self.genesis_block.timestamp,
|
timestamp: Some(self.genesis_block.timestamp),
|
||||||
parent_hash: self.genesis_block.parent_hash.clone(),
|
parent_hash: Some(self.genesis_block.parent_hash.clone()),
|
||||||
gas_limit: self.genesis_block.gas_limit,
|
gas_limit: self.genesis_block.gas_limit,
|
||||||
transactions_root: Some(self.genesis_block.transactions_root.clone()),
|
transactions_root: Some(self.genesis_block.transactions_root.clone()),
|
||||||
receipts_root: Some(self.genesis_block.receipts_root.clone()),
|
receipts_root: Some(self.genesis_block.receipts_root.clone()),
|
||||||
|
@ -28,13 +28,13 @@ pub struct Genesis {
|
|||||||
pub seal: Seal,
|
pub seal: Seal,
|
||||||
/// Difficulty.
|
/// Difficulty.
|
||||||
pub difficulty: Uint,
|
pub difficulty: Uint,
|
||||||
/// Block author.
|
/// Block author, defaults to 0.
|
||||||
pub author: Address,
|
pub author: Option<Address>,
|
||||||
/// Block timestamp.
|
/// Block timestamp, defaults to 0.
|
||||||
pub timestamp: Uint,
|
pub timestamp: Option<Uint>,
|
||||||
/// Parent hash.
|
/// Parent hash, defaults to 0.
|
||||||
#[serde(rename="parentHash")]
|
#[serde(rename="parentHash")]
|
||||||
pub parent_hash: H256,
|
pub parent_hash: Option<H256>,
|
||||||
/// Gas limit.
|
/// Gas limit.
|
||||||
#[serde(rename="gasLimit")]
|
#[serde(rename="gasLimit")]
|
||||||
pub gas_limit: Uint,
|
pub gas_limit: Uint,
|
||||||
|
@ -22,9 +22,9 @@ use hash::H256;
|
|||||||
/// Spec params.
|
/// Spec params.
|
||||||
#[derive(Debug, PartialEq, Deserialize)]
|
#[derive(Debug, PartialEq, Deserialize)]
|
||||||
pub struct Params {
|
pub struct Params {
|
||||||
/// Account start nonce.
|
/// Account start nonce, defaults to 0.
|
||||||
#[serde(rename="accountStartNonce")]
|
#[serde(rename="accountStartNonce")]
|
||||||
pub account_start_nonce: Uint,
|
pub account_start_nonce: Option<Uint>,
|
||||||
/// Maximum size of extra data.
|
/// Maximum size of extra data.
|
||||||
#[serde(rename="maximumExtraDataSize")]
|
#[serde(rename="maximumExtraDataSize")]
|
||||||
pub maximum_extra_data_size: Uint,
|
pub maximum_extra_data_size: Uint,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user