Merge branch 'master' into lightrpc

This commit is contained in:
Robert Habermeier 2017-02-10 02:44:02 +01:00
commit 6bf97de9d8
90 changed files with 2082 additions and 636 deletions

55
Cargo.lock generated
View File

@ -122,6 +122,14 @@ dependencies = [
"rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "bit-set"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bit-vec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "bit-set"
version = "0.4.0"
@ -381,6 +389,7 @@ dependencies = [
"ethkey 0.2.0",
"ethstore 0.1.0",
"evmjit 1.6.0",
"hardware-wallet 1.6.0",
"heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -863,6 +872,18 @@ name = "hamming"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "hardware-wallet"
version = "1.6.0"
dependencies = [
"ethcore-bigint 0.1.2",
"ethkey 0.2.0",
"hidapi 0.3.1 (git+https://github.com/ethcore/hidapi-rs)",
"libusb 0.3.0 (git+https://github.com/ethcore/libusb-rs)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "heapsize"
version = "0.3.6"
@ -871,6 +892,15 @@ dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "hidapi"
version = "0.3.1"
source = "git+https://github.com/ethcore/hidapi-rs#9a127c1dca7e327e4fdd428406a76c9f5ef48563"
dependencies = [
"gcc 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "hpack"
version = "0.2.0"
@ -1108,6 +1138,25 @@ name = "libc"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "libusb"
version = "0.3.0"
source = "git+https://github.com/ethcore/libusb-rs#32bacf61abd981d5cbd4a8fecca5a2dc0b762a96"
dependencies = [
"bit-set 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"libusb-sys 0.2.3 (git+https://github.com/ethcore/libusb-sys)",
]
[[package]]
name = "libusb-sys"
version = "0.2.3"
source = "git+https://github.com/ethcore/libusb-sys#3c56a34bd040b0e60758824d839d9b700cfad2e0"
dependencies = [
"gcc 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "linked-hash-map"
version = "0.2.1"
@ -1575,7 +1624,7 @@ dependencies = [
[[package]]
name = "parity-ui-precompiled"
version = "1.4.0"
source = "git+https://github.com/ethcore/js-precompiled.git#cb0dd77b70c552bb68288a94c7d5d37ecdd611c8"
source = "git+https://github.com/ethcore/js-precompiled.git#8d3275d4b227ea47558d6d8c423cc23b6bb504f3"
dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -2487,6 +2536,7 @@ dependencies = [
"checksum aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07d344974f0a155f091948aa389fb1b912d3a58414fbdb9c8d446d193ee3496a"
"checksum base32 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1b9605ba46d61df0410d8ac686b0007add8172eba90e8e909c347856fe794d8c"
"checksum bigint 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2311bcd71b281e142a095311c22509f0d6bcd87b3000d7dbaa810929b9d6f6ae"
"checksum bit-set 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e6e1e6fb1c9e3d6fcdec57216a74eaa03e41f52a22f13a16438251d8e88b89da"
"checksum bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9bf6104718e80d7b26a68fdbacff3481cfc05df670821affc7e9cbc1884400c"
"checksum bit-vec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5b97c2c8e8bbb4251754f559df8af22fb264853c7d009084a576cdf12565089d"
"checksum bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3"
@ -2526,6 +2576,7 @@ dependencies = [
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
"checksum hamming 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65043da274378d68241eb9a8f8f8aa54e349136f7b8e12f63e3ef44043cc30e1"
"checksum heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "abb306abb8d398e053cfb1b3e7b72c2f580be048b85745c52652954f8ad1439c"
"checksum hidapi 0.3.1 (git+https://github.com/ethcore/hidapi-rs)" = "<none>"
"checksum hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2da7d3a34cf6406d9d700111b8eafafe9a251de41ae71d8052748259343b58"
"checksum httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "46534074dbb80b070d60a5cb8ecadd8963a00a438ae1a95268850a7ef73b67ae"
"checksum hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)" = "<none>"
@ -2548,6 +2599,8 @@ dependencies = [
"checksum lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "49247ec2a285bb3dcb23cbd9c35193c025e7251bfce77c1d5da97e6362dffe7f"
"checksum lazycell 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce12306c4739d86ee97c23139f3a34ddf0387bbf181bc7929d287025a8c3ef6b"
"checksum libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)" = "408014cace30ee0f767b1c4517980646a573ec61a57957aeeabcac8ac0a02e8d"
"checksum libusb 0.3.0 (git+https://github.com/ethcore/libusb-rs)" = "<none>"
"checksum libusb-sys 0.2.3 (git+https://github.com/ethcore/libusb-sys)" = "<none>"
"checksum linked-hash-map 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bda158e0dabeb97ee8a401f4d17e479d6b891a14de0bba79d5cc2d4d325b5e48"
"checksum linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd"
"checksum log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ab83497bf8bf4ed2a74259c1c802351fcd67a65baa86394b6ba73c36f4838054"

View File

@ -42,6 +42,7 @@ rlp = { path = "../util/rlp" }
ethcore-stratum = { path = "../stratum" }
lru-cache = "0.1.0"
ethcore-bloom-journal = { path = "../util/bloom" }
hardware-wallet = { path = "../hw" }
ethabi = "0.2.2"
[dependencies.hyper]

View File

@ -29,6 +29,7 @@ use ethstore::{SimpleSecretStore, SecretStore, Error as SSError, EthStore, EthMu
use ethstore::dir::MemoryDirectory;
use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator};
use ethjson::misc::AccountMeta;
use hardware_wallet::{Error as HardwareError, HardwareWalletManager, KeyPath};
pub use ethstore::ethkey::Signature;
/// Type of unlock.
@ -55,6 +56,10 @@ struct AccountData {
pub enum SignError {
/// Account is not unlocked
NotUnlocked,
/// Account does not exist.
NotFound,
/// Low-level hardware device error.
Hardware(HardwareError),
/// Low-level error from store
SStore(SSError)
}
@ -63,11 +68,19 @@ impl fmt::Display for SignError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
SignError::NotUnlocked => write!(f, "Account is locked"),
SignError::NotFound => write!(f, "Account does not exist"),
SignError::Hardware(ref e) => write!(f, "{}", e),
SignError::SStore(ref e) => write!(f, "{}", e),
}
}
}
impl From<HardwareError> for SignError {
fn from(e: HardwareError) -> Self {
SignError::Hardware(e)
}
}
impl From<SSError> for SignError {
fn from(e: SSError) -> Self {
SignError::SStore(e)
@ -107,17 +120,47 @@ pub struct AccountProvider {
sstore: Box<SecretStore>,
/// Accounts unlocked with rolling tokens
transient_sstore: EthMultiStore,
/// Accounts in hardware wallets.
hardware_store: Option<HardwareWalletManager>,
}
/// Account management settings.
pub struct AccountProviderSettings {
/// Enable hardware wallet support.
pub enable_hardware_wallets: bool,
/// Use the classic chain key on the hardware wallet.
pub hardware_wallet_classic_key: bool,
}
impl Default for AccountProviderSettings {
fn default() -> Self {
AccountProviderSettings {
enable_hardware_wallets: false,
hardware_wallet_classic_key: false,
}
}
}
impl AccountProvider {
/// Creates new account provider.
pub fn new(sstore: Box<SecretStore>) -> Self {
pub fn new(sstore: Box<SecretStore>, settings: AccountProviderSettings) -> Self {
let mut hardware_store = None;
if settings.enable_hardware_wallets {
match HardwareWalletManager::new() {
Ok(manager) => {
manager.set_key_path(if settings.hardware_wallet_classic_key { KeyPath::EthereumClassic } else { KeyPath::Ethereum });
hardware_store = Some(manager)
},
Err(e) => warn!("Error initializing hardware wallets: {}", e),
}
}
AccountProvider {
unlocked: RwLock::new(HashMap::new()),
address_book: RwLock::new(AddressBook::new(&sstore.local_path())),
dapps_settings: RwLock::new(DappsSettingsStore::new(&sstore.local_path())),
sstore: sstore,
transient_sstore: transient_sstore(),
hardware_store: hardware_store,
}
}
@ -129,6 +172,7 @@ impl AccountProvider {
dapps_settings: RwLock::new(DappsSettingsStore::transient()),
sstore: Box::new(EthStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory load always succeeds; qed")),
transient_sstore: transient_sstore(),
hardware_store: None,
}
}
@ -176,6 +220,12 @@ impl AccountProvider {
Ok(accounts.into_iter().map(|a| a.address).collect())
}
/// Returns addresses of hardware accounts.
pub fn hardware_accounts(&self) -> Result<Vec<Address>, Error> {
let accounts = self.hardware_store.as_ref().map_or(Vec::new(), |h| h.list_wallets());
Ok(accounts.into_iter().map(|a| a.address).collect())
}
/// Sets a whitelist of accounts exposed for unknown dapps.
/// `None` means that all accounts will be visible.
pub fn set_new_dapps_whitelist(&self, accounts: Option<Vec<Address>>) -> Result<(), Error> {
@ -271,15 +321,36 @@ impl AccountProvider {
/// Returns each account along with name and meta.
pub fn accounts_info(&self) -> Result<HashMap<Address, AccountMeta>, Error> {
let r: HashMap<Address, AccountMeta> = self.sstore.accounts()?
let r = self.sstore.accounts()?
.into_iter()
.map(|a| (a.address.clone(), self.account_meta(a.address).ok().unwrap_or_default()))
.collect();
Ok(r)
}
/// Returns each hardware account along with name and meta.
pub fn hardware_accounts_info(&self) -> Result<HashMap<Address, AccountMeta>, Error> {
let r = self.hardware_accounts()?
.into_iter()
.map(|address| (address.clone(), self.account_meta(address).ok().unwrap_or_default()))
.collect();
Ok(r)
}
/// Returns each hardware account along with name and meta.
pub fn is_hardware_address(&self, address: Address) -> bool {
self.hardware_store.as_ref().and_then(|s| s.wallet_info(&address)).is_some()
}
/// Returns each account along with name and meta.
pub fn account_meta(&self, address: Address) -> Result<AccountMeta, Error> {
if let Some(info) = self.hardware_store.as_ref().and_then(|s| s.wallet_info(&address)) {
Ok(AccountMeta {
name: info.name,
meta: info.manufacturer,
uuid: None,
})
} else {
let account = self.sstore.account_ref(&address)?;
Ok(AccountMeta {
name: self.sstore.name(&account)?,
@ -287,6 +358,7 @@ impl AccountProvider {
uuid: self.sstore.uuid(&account).ok().map(Into::into), // allowed to not have a Uuid
})
}
}
/// Returns each account along with name and meta.
pub fn set_account_name(&self, address: Address, name: String) -> Result<(), Error> {
@ -505,6 +577,15 @@ impl AccountProvider {
self.sstore.set_vault_meta(name, meta)
.map_err(Into::into)
}
/// Sign transaction with hardware wallet.
pub fn sign_with_hardware(&self, address: Address, transaction: &[u8]) -> Result<Signature, SignError> {
match self.hardware_store.as_ref().map(|s| s.sign_transaction(&address, transaction)) {
None | Some(Err(HardwareError::KeyNotFound)) => Err(SignError::NotFound),
Some(Err(e)) => Err(From::from(e)),
Some(Ok(s)) => Ok(s),
}
}
}
#[cfg(test)]

View File

@ -105,6 +105,7 @@ extern crate linked_hash_map;
extern crate lru_cache;
extern crate ethcore_stratum;
extern crate ethabi;
extern crate hardware_wallet;
#[macro_use]
extern crate log;

View File

@ -154,7 +154,7 @@ impl Transaction {
pub fn hash(&self, network_id: Option<u64>) -> H256 {
let mut stream = RlpStream::new();
self.rlp_append_unsigned_transaction(&mut stream, network_id);
stream.out().sha3()
stream.as_raw().sha3()
}
/// Signs the transaction as coming from `sender`.

View File

@ -21,6 +21,53 @@ use Public;
use bigint::hash::{H256, FixedHash};
pub use self::derivation::Error as DerivationError;
/// Represents label that can be stored as a part of key derivation
pub trait Label {
/// Length of the data that label occupies
fn len() -> usize;
/// Store label data to the key derivation sequence
/// Must not use more than `len()` bytes from slice
fn store(&self, target: &mut [u8]);
}
impl Label for u32 {
fn len() -> usize { 4 }
fn store(&self, target: &mut [u8]) {
use byteorder::{BigEndian, ByteOrder};
BigEndian::write_u32(&mut target[0..4], *self);
}
}
/// Key derivation over generic label `T`
pub enum Derivation<T: Label> {
/// Soft key derivation (allow proof of parent)
Soft(T),
/// Hard key derivation (does not allow proof of parent)
Hard(T),
}
impl From<u32> for Derivation<u32> {
fn from(index: u32) -> Self {
if index < (2 << 30) {
Derivation::Soft(index)
}
else {
Derivation::Hard(index)
}
}
}
impl Label for H256 {
fn len() -> usize { 32 }
fn store(&self, target: &mut [u8]) {
self.copy_to(&mut target[0..32]);
}
}
/// Extended secret key, allows deterministic derivation of subsequent keys.
pub struct ExtendedSecret {
secret: Secret,
@ -49,7 +96,7 @@ impl ExtendedSecret {
}
/// Derive new private key
pub fn derive(&self, index: u32) -> ExtendedSecret {
pub fn derive<T>(&self, index: Derivation<T>) -> ExtendedSecret where T: Label {
let (derived_key, next_chain_code) = derivation::private(*self.secret, self.chain_code, index);
let derived_secret = Secret::from_slice(&*derived_key)
@ -88,7 +135,7 @@ impl ExtendedPublic {
/// Derive new public key
/// Operation is defined only for index belongs [0..2^31)
pub fn derive(&self, index: u32) -> Result<Self, DerivationError> {
pub fn derive<T>(&self, index: Derivation<T>) -> Result<Self, DerivationError> where T: Label {
let (derived_key, next_chain_code) = derivation::public(self.public, self.chain_code, index)?;
Ok(ExtendedPublic::new(derived_key, next_chain_code))
}
@ -147,7 +194,7 @@ impl ExtendedKeyPair {
&self.public
}
pub fn derive(&self, index: u32) -> Result<Self, DerivationError> {
pub fn derive<T>(&self, index: Derivation<T>) -> Result<Self, DerivationError> where T: Label {
let derived = self.secret.derive(index);
Ok(ExtendedKeyPair {
@ -167,11 +214,11 @@ mod derivation {
use rcrypto::sha2::Sha512;
use bigint::hash::{H512, H256, FixedHash};
use bigint::prelude::{U256, U512, Uint};
use byteorder::{BigEndian, ByteOrder};
use secp256k1;
use secp256k1::key::{SecretKey, PublicKey};
use SECP256K1;
use keccak;
use super::{Label, Derivation};
#[derive(Debug)]
pub enum Error {
@ -183,20 +230,18 @@ mod derivation {
// Deterministic derivation of the key using secp256k1 elliptic curve.
// Derivation can be either hardened or not.
// For hardened derivation, pass index at least 2^31
// For hardened derivation, pass u32 index at least 2^31 or custom Derivation::Hard(T) enum
//
// Can panic if passed `private_key` is not a valid secp256k1 private key
// (outside of (0..curve_n()]) field
pub fn private(private_key: H256, chain_code: H256, index: u32) -> (H256, H256) {
if index < (2 << 30) {
private_soft(private_key, chain_code, index)
}
else {
private_hard(private_key, chain_code, index)
pub fn private<T>(private_key: H256, chain_code: H256, index: Derivation<T>) -> (H256, H256) where T: Label {
match index {
Derivation::Soft(index) => private_soft(private_key, chain_code, index),
Derivation::Hard(index) => private_hard(private_key, chain_code, index),
}
}
fn hmac_pair(data: [u8; 37], private_key: H256, chain_code: H256) -> (H256, H256) {
fn hmac_pair(data: &[u8], private_key: H256, chain_code: H256) -> (H256, H256) {
let private: U256 = private_key.into();
// produces 512-bit derived hmac (I)
@ -216,8 +261,8 @@ mod derivation {
// Can panic if passed `private_key` is not a valid secp256k1 private key
// (outside of (0..curve_n()]) field
fn private_soft(private_key: H256, chain_code: H256, index: u32) -> (H256, H256) {
let mut data = [0u8; 37];
fn private_soft<T>(private_key: H256, chain_code: H256, index: T) -> (H256, H256) where T: Label {
let mut data = vec![0u8; 33 + T::len()];
let sec_private = SecretKey::from_slice(&SECP256K1, &*private_key)
.expect("Caller should provide valid private key");
@ -226,26 +271,26 @@ mod derivation {
let public_serialized = sec_public.serialize_vec(&SECP256K1, true);
// curve point (compressed public key) -- index
// 0.33 -- 33..37
// 0.33 -- 33..end
data[0..33].copy_from_slice(&public_serialized);
BigEndian::write_u32(&mut data[33..37], index);
index.store(&mut data[33..]);
hmac_pair(data, private_key, chain_code)
hmac_pair(&data, private_key, chain_code)
}
// Deterministic derivation of the key using secp256k1 elliptic curve
// This is hardened derivation and does not allow to associate
// corresponding public keys of the original and derived private keys
fn private_hard(private_key: H256, chain_code: H256, index: u32) -> (H256, H256) {
let mut data = [0u8; 37];
fn private_hard<T>(private_key: H256, chain_code: H256, index: T) -> (H256, H256) where T: Label {
let mut data: Vec<u8> = vec![0u8; 33 + T::len()];
let private: U256 = private_key.into();
// 0x00 (padding) -- private_key -- index
// 0 -- 1..33 -- 33..37
// 0 -- 1..33 -- 33..end
private.to_big_endian(&mut data[1..33]);
BigEndian::write_u32(&mut data[33..37], index);
index.store(&mut data[33..(33 + T::len())]);
hmac_pair(data, private_key, chain_code)
hmac_pair(&data, private_key, chain_code)
}
fn private_add(k1: U256, k2: U256) -> U256 {
@ -266,11 +311,11 @@ mod derivation {
H256::from_slice(&secp256k1::constants::CURVE_ORDER).into()
}
pub fn public(public_key: H512, chain_code: H256, index: u32) -> Result<(H512, H256), Error> {
if index >= (2 << 30) {
// public derivation is only defined on 'soft' index space [0..2^31)
return Err(Error::InvalidHardenedUse)
}
pub fn public<T>(public_key: H512, chain_code: H256, derivation: Derivation<T>) -> Result<(H512, H256), Error> where T: Label {
let index = match derivation {
Derivation::Soft(index) => index,
Derivation::Hard(_) => { return Err(Error::InvalidHardenedUse); }
};
let mut public_sec_raw = [0u8; 65];
public_sec_raw[0] = 4;
@ -278,11 +323,11 @@ mod derivation {
let public_sec = PublicKey::from_slice(&SECP256K1, &public_sec_raw).map_err(|_| Error::InvalidPoint)?;
let public_serialized = public_sec.serialize_vec(&SECP256K1, true);
let mut data = [0u8; 37];
let mut data = vec![0u8; 33 + T::len()];
// curve point (compressed public key) -- index
// 0.33 -- 33..37
// 0.33 -- 33..end
data[0..33].copy_from_slice(&public_serialized);
BigEndian::write_u32(&mut data[33..37], index);
index.store(&mut data[33..(33 + T::len())]);
// HMAC512SHA produces [derived private(256); new chain code(256)]
let mut hmac = Hmac::new(Sha512::new(), &*chain_code);
@ -351,7 +396,7 @@ mod tests {
use secret::Secret;
use std::str::FromStr;
use bigint::hash::{H128, H256};
use super::derivation;
use super::{derivation, Derivation};
fn master_chain_basic() -> (H256, H256) {
let seed = H128::from_str("000102030405060708090a0b0c0d0e0f")
@ -375,23 +420,48 @@ mod tests {
// hardened
assert_eq!(&**extended_secret.secret(), &*secret);
assert_eq!(&**extended_secret.derive(2147483648).secret(), &"0927453daed47839608e414a3738dfad10aed17c459bbd9ab53f89b026c834b6".into());
assert_eq!(&**extended_secret.derive(2147483649).secret(), &"44238b6a29c6dcbe9b401364141ba11e2198c289a5fed243a1c11af35c19dc0f".into());
assert_eq!(&**extended_secret.derive(2147483648.into()).secret(), &"0927453daed47839608e414a3738dfad10aed17c459bbd9ab53f89b026c834b6".into());
assert_eq!(&**extended_secret.derive(2147483649.into()).secret(), &"44238b6a29c6dcbe9b401364141ba11e2198c289a5fed243a1c11af35c19dc0f".into());
// normal
assert_eq!(&**extended_secret.derive(0).secret(), &"bf6a74e3f7b36fc4c96a1e12f31abc817f9f5904f5a8fc27713163d1f0b713f6".into());
assert_eq!(&**extended_secret.derive(1).secret(), &"bd4fca9eb1f9c201e9448c1eecd66e302d68d4d313ce895b8c134f512205c1bc".into());
assert_eq!(&**extended_secret.derive(2).secret(), &"86932b542d6cab4d9c65490c7ef502d89ecc0e2a5f4852157649e3251e2a3268".into());
assert_eq!(&**extended_secret.derive(0.into()).secret(), &"bf6a74e3f7b36fc4c96a1e12f31abc817f9f5904f5a8fc27713163d1f0b713f6".into());
assert_eq!(&**extended_secret.derive(1.into()).secret(), &"bd4fca9eb1f9c201e9448c1eecd66e302d68d4d313ce895b8c134f512205c1bc".into());
assert_eq!(&**extended_secret.derive(2.into()).secret(), &"86932b542d6cab4d9c65490c7ef502d89ecc0e2a5f4852157649e3251e2a3268".into());
let extended_public = ExtendedPublic::from_secret(&extended_secret).expect("Extended public should be created");
let derived_public = extended_public.derive(0).expect("First derivation of public should succeed");
let derived_public = extended_public.derive(0.into()).expect("First derivation of public should succeed");
assert_eq!(&*derived_public.public(), &"f7b3244c96688f92372bfd4def26dc4151529747bab9f188a4ad34e141d47bd66522ff048bc6f19a0a4429b04318b1a8796c000265b4fa200dae5f6dda92dd94".into());
let keypair = ExtendedKeyPair::with_secret(
Secret::from_str("a100df7a048e50ed308ea696dc600215098141cb391e9527329df289f9383f65").unwrap(),
064.into(),
);
assert_eq!(&**keypair.derive(2147483648).expect("Derivation of keypair should succeed").secret().secret(), &"edef54414c03196557cf73774bc97a645c9a1df2164ed34f0c2a78d1375a930c".into());
assert_eq!(&**keypair.derive(2147483648u32.into()).expect("Derivation of keypair should succeed").secret().secret(), &"edef54414c03196557cf73774bc97a645c9a1df2164ed34f0c2a78d1375a930c".into());
}
#[test]
fn h256_soft_match() {
let secret = Secret::from_str("a100df7a048e50ed308ea696dc600215098141cb391e9527329df289f9383f65").unwrap();
let derivation_secret = H256::from_str("51eaf04f9dbbc1417dc97e789edd0c37ecda88bac490434e367ea81b71b7b015").unwrap();
let extended_secret = ExtendedSecret::with_code(secret.clone(), 0u64.into());
let extended_public = ExtendedPublic::from_secret(&extended_secret).expect("Extended public should be created");
let derived_secret0 = extended_secret.derive(Derivation::Soft(derivation_secret));
let derived_public0 = extended_public.derive(Derivation::Soft(derivation_secret)).expect("First derivation of public should succeed");
let public_from_secret0 = ExtendedPublic::from_secret(&derived_secret0).expect("Extended public should be created");
assert_eq!(public_from_secret0.public(), derived_public0.public());
}
#[test]
fn h256_hard() {
let secret = Secret::from_str("a100df7a048e50ed308ea696dc600215098141cb391e9527329df289f9383f65").unwrap();
let derivation_secret = H256::from_str("51eaf04f9dbbc1417dc97e789edd0c37ecda88bac490434e367ea81b71b7b015").unwrap();
let extended_secret = ExtendedSecret::with_code(secret.clone(), 1u64.into());
assert_eq!(&**extended_secret.derive(Derivation::Hard(derivation_secret)).secret(), &"2bc2d696fb744d77ff813b4a1ef0ad64e1e5188b622c54ba917acc5ebc7c5486".into());
}
#[test]
@ -400,8 +470,8 @@ mod tests {
let extended_secret = ExtendedSecret::with_code(secret.clone(), 1.into());
let extended_public = ExtendedPublic::from_secret(&extended_secret).expect("Extended public should be created");
let derived_secret0 = extended_secret.derive(0);
let derived_public0 = extended_public.derive(0).expect("First derivation of public should succeed");
let derived_secret0 = extended_secret.derive(0.into());
let derived_public0 = extended_public.derive(0.into()).expect("First derivation of public should succeed");
let public_from_secret0 = ExtendedPublic::from_secret(&derived_secret0).expect("Extended public should be created");
@ -429,7 +499,7 @@ mod tests {
/// xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7
/// H(0)
test_extended(
|secret| secret.derive(2147483648),
|secret| secret.derive(2147483648.into()),
H256::from_str("edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea")
.expect("Private should be decoded ok")
);
@ -440,7 +510,7 @@ mod tests {
/// xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs
/// H(0)/1
test_extended(
|secret| secret.derive(2147483648).derive(1),
|secret| secret.derive(2147483648.into()).derive(1.into()),
H256::from_str("3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368")
.expect("Private should be decoded ok")
);

View File

@ -22,8 +22,9 @@ use std::{env, process, fs};
use std::io::Read;
use docopt::Docopt;
use ethstore::ethkey::Address;
use ethstore::dir::{KeyDirectory, ParityDirectory, DiskDirectory, GethDirectory, DirectoryType};
use ethstore::{EthStore, SecretStore, import_accounts, Error, PresaleWallet};
use ethstore::dir::{KeyDirectory, ParityDirectory, RootDiskDirectory, GethDirectory, DirectoryType};
use ethstore::{EthStore, SimpleSecretStore, SecretStore, import_accounts, Error, PresaleWallet,
SecretVaultRef, StoreAccountRef};
pub const USAGE: &'static str = r#"
Ethereum key management.
@ -97,7 +98,7 @@ fn key_dir(location: &str) -> Result<Box<KeyDirectory>, Error> {
"parity-test" => Box::new(ParityDirectory::create(DirectoryType::Testnet)?),
"geth" => Box::new(GethDirectory::create(DirectoryType::Main)?),
"geth-test" => Box::new(GethDirectory::create(DirectoryType::Testnet)?),
path => Box::new(DiskDirectory::create(path)?),
path => Box::new(RootDiskDirectory::create(path)?),
};
Ok(dir)
@ -130,16 +131,17 @@ fn execute<S, I>(command: I) -> Result<String, Error> where I: IntoIterator<Item
return if args.cmd_insert {
let secret = args.arg_secret.parse().map_err(|_| Error::InvalidSecret)?;
let password = load_password(&args.arg_password)?;
let address = store.insert_account(secret, &password)?;
let address = store.insert_account(SecretVaultRef::Root, secret, &password)?;
Ok(format!("0x{:?}", address))
} else if args.cmd_change_pwd {
let address = args.arg_address.parse().map_err(|_| Error::InvalidAccount)?;
let old_pwd = load_password(&args.arg_old_pwd)?;
let new_pwd = load_password(&args.arg_new_pwd)?;
let ok = store.change_password(&address, &old_pwd, &new_pwd).is_ok();
let ok = store.change_password(&StoreAccountRef::root(address), &old_pwd, &new_pwd).is_ok();
Ok(format!("{}", ok))
} else if args.cmd_list {
let accounts = store.accounts()?;
let accounts: Vec<_> = accounts.into_iter().map(|a| a.address).collect();
Ok(format_accounts(&accounts))
} else if args.cmd_import {
let src = key_dir(&args.flag_src)?;
@ -150,23 +152,23 @@ fn execute<S, I>(command: I) -> Result<String, Error> where I: IntoIterator<Item
let wallet = PresaleWallet::open(&args.arg_path)?;
let password = load_password(&args.arg_password)?;
let kp = wallet.decrypt(&password)?;
let address = store.insert_account(kp.secret().clone(), &password)?;
let address = store.insert_account(SecretVaultRef::Root, kp.secret().clone(), &password)?;
Ok(format!("0x{:?}", address))
} else if args.cmd_remove {
let address = args.arg_address.parse().map_err(|_| Error::InvalidAccount)?;
let password = load_password(&args.arg_password)?;
let ok = store.remove_account(&address, &password).is_ok();
let ok = store.remove_account(&StoreAccountRef::root(address), &password).is_ok();
Ok(format!("{}", ok))
} else if args.cmd_sign {
let address = args.arg_address.parse().map_err(|_| Error::InvalidAccount)?;
let message = args.arg_message.parse().map_err(|_| Error::InvalidMessage)?;
let password = load_password(&args.arg_password)?;
let signature = store.sign(&address, &password, &message)?;
let signature = store.sign(&StoreAccountRef::root(address), &password, &message)?;
Ok(format!("0x{:?}", signature))
} else if args.cmd_public {
let address = args.arg_address.parse().map_err(|_| Error::InvalidAccount)?;
let password = load_password(&args.arg_password)?;
let public = store.public(&address, &password)?;
let public = store.public(&StoreAccountRef::root(address), &password)?;
Ok(format!("0x{:?}", public))
} else {
Ok(format!("{}", USAGE))

View File

@ -234,6 +234,10 @@ impl<T> VaultKeyDirectoryProvider for DiskDirectory<T> where T: KeyFileManager {
})
.collect())
}
fn vault_meta(&self, name: &str) -> Result<String, Error> {
VaultDiskDirectory::meta_at(&self.path, name)
}
}
impl KeyFileManager for DiskKeyFileManager {
@ -242,7 +246,12 @@ impl KeyFileManager for DiskKeyFileManager {
Ok(SafeAccount::from_file(key_file, filename))
}
fn write<T>(&self, account: SafeAccount, writer: &mut T) -> Result<(), Error> where T: io::Write {
fn write<T>(&self, mut account: SafeAccount, writer: &mut T) -> Result<(), Error> where T: io::Write {
// when account is moved back to root directory from vault
// => remove vault field from meta
account.meta = json::remove_vault_name_from_json_meta(&account.meta)
.map_err(|err| Error::Custom(format!("{:?}", err)))?;
let key_file: json::KeyFile = account.into();
key_file.write(writer).map_err(|e| Error::Custom(format!("{:?}", e)))
}

View File

@ -72,6 +72,8 @@ pub trait VaultKeyDirectoryProvider {
fn open(&self, name: &str, key: VaultKey) -> Result<Box<VaultKeyDirectory>, Error>;
/// List all vaults
fn list_vaults(&self) -> Result<Vec<String>, Error>;
/// Get vault meta
fn vault_meta(&self, name: &str) -> Result<String, Error>;
}
/// Vault directory

View File

@ -67,11 +67,23 @@ impl VaultDiskDirectory {
}
// check that passed key matches vault file
let meta = read_vault_file(&vault_dir_path, &key)?;
let meta = read_vault_file(&vault_dir_path, Some(&key))?;
Ok(DiskDirectory::new(vault_dir_path, VaultKeyFileManager::new(name, key, &meta)))
}
/// Read vault meta without actually opening the vault
pub fn meta_at<P>(root: P, name: &str) -> Result<String, Error> where P: AsRef<Path> {
// check that vault directory exists
let vault_dir_path = make_vault_dir_path(root, name, true)?;
if !vault_dir_path.is_dir() {
return Err(Error::VaultNotFound);
}
// check that passed key matches vault file
read_vault_file(&vault_dir_path, None)
}
fn create_temp_vault(&self, key: VaultKey) -> Result<VaultDiskDirectory, Error> {
let original_path = self.path().expect("self is instance of DiskDirectory; DiskDirectory always returns path; qed");
let mut path: PathBuf = original_path.clone();
@ -241,7 +253,7 @@ fn create_vault_file<P>(vault_dir_path: P, key: &VaultKey, meta: &str) -> Result
}
/// When vault is opened => we must check that password matches && read metadata
fn read_vault_file<P>(vault_dir_path: P, key: &VaultKey) -> Result<String, Error> where P: AsRef<Path> {
fn read_vault_file<P>(vault_dir_path: P, key: Option<&VaultKey>) -> Result<String, Error> where P: AsRef<Path> {
let mut vault_file_path: PathBuf = vault_dir_path.as_ref().into();
vault_file_path.push(VAULT_FILE_NAME);
@ -250,11 +262,13 @@ fn read_vault_file<P>(vault_dir_path: P, key: &VaultKey) -> Result<String, Error
let vault_file_meta = vault_file_contents.meta.unwrap_or("{}".to_owned());
let vault_file_crypto: Crypto = vault_file_contents.crypto.into();
if let Some(key) = key {
let password_bytes = vault_file_crypto.decrypt(&key.password)?;
let password_hash = key.password.sha3();
if &*password_hash != password_bytes.as_slice() {
return Err(Error::InvalidPassword);
}
}
Ok(vault_file_meta)
}
@ -264,7 +278,7 @@ mod test {
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use dir::{VaultKey, VaultKeyDirectory};
use dir::VaultKey;
use super::{VAULT_FILE_NAME, check_vault_name, make_vault_dir_path, create_vault_file, read_vault_file, VaultDiskDirectory};
use devtools::RandomTempPath;
@ -333,7 +347,7 @@ mod test {
}
// when
let result = read_vault_file(&dir, &key);
let result = read_vault_file(&dir, Some(&key));
// then
assert!(result.is_ok());
@ -349,7 +363,7 @@ mod test {
vault_file_path.push(VAULT_FILE_NAME);
// when
let result = read_vault_file(&dir, &key);
let result = read_vault_file(&dir, Some(&key));
// then
assert!(result.is_err());
@ -362,7 +376,7 @@ mod test {
}
// when
let result = read_vault_file(&dir, &key);
let result = read_vault_file(&dir, Some(&key));
// then
assert!(result.is_err());
@ -418,22 +432,4 @@ mod test {
// then
assert!(vault.is_err());
}
#[test]
fn vault_directory_can_preserve_meta() {
// given
let temp_path = RandomTempPath::new();
let key = VaultKey::new("password", 1024);
let dir: PathBuf = temp_path.as_path().into();
let vault = VaultDiskDirectory::create(&dir, "vault", key.clone()).unwrap();
// then
assert_eq!(vault.meta(), "{}".to_owned());
assert!(vault.set_meta("Hello, world!!!").is_ok());
assert_eq!(vault.meta(), "Hello, world!!!".to_owned());
// and when
let vault = VaultDiskDirectory::at(&dir, "vault", key.clone()).unwrap();
assert_eq!(vault.meta(), "Hello, world!!!".to_owned());
}
}

View File

@ -501,10 +501,17 @@ impl SimpleSecretStore for EthMultiStore {
}
fn get_vault_meta(&self, name: &str) -> Result<String, Error> {
// vault meta contains password hint
// => allow reading meta even if vault is not yet opened
self.vaults.lock()
.get(name)
.and_then(|v| Some(v.meta()))
.ok_or(Error::VaultNotFound)
.and_then(|v| Ok(v.meta()))
.or_else(|_| {
let vault_provider = self.dir.as_vault_provider().ok_or(Error::VaultsAreNotSupported)?;
vault_provider.vault_meta(name)
})
}
fn set_vault_meta(&self, name: &str, meta: &str) -> Result<(), Error> {
@ -861,4 +868,34 @@ mod tests {
assert!(opened_vaults.iter().any(|v| &*v == name1));
assert!(opened_vaults.iter().any(|v| &*v == name3));
}
#[test]
fn should_manage_vaults_meta() {
// given
let mut dir = RootDiskDirectoryGuard::new();
let store = EthStore::open(dir.key_dir.take().unwrap()).unwrap();
let name1 = "vault1"; let password1 = "password1";
// when
store.create_vault(name1, password1).unwrap();
// then
assert_eq!(store.get_vault_meta(name1).unwrap(), "{}".to_owned());
assert!(store.set_vault_meta(name1, "Hello, world!!!").is_ok());
assert_eq!(store.get_vault_meta(name1).unwrap(), "Hello, world!!!".to_owned());
// and when
store.close_vault(name1).unwrap();
store.open_vault(name1, password1).unwrap();
// then
assert_eq!(store.get_vault_meta(name1).unwrap(), "Hello, world!!!".to_owned());
// and when
store.close_vault(name1).unwrap();
// then
assert_eq!(store.get_vault_meta(name1).unwrap(), "Hello, world!!!".to_owned());
assert!(store.get_vault_meta("vault2").is_err());
}
}

View File

@ -81,7 +81,7 @@ impl Visitor for VaultFileVisitor {
loop {
match visitor.visit_key()? {
Some(VaultFileField::Crypto) => { crypto = Some(visitor.visit_value()?); },
Some(VaultFileField::Meta) => { meta = Some(visitor.visit_value()?); }
Some(VaultFileField::Meta) => { meta = visitor.visit_value().ok(); }, // meta is optional
None => { break; },
}
}
@ -141,4 +141,29 @@ mod test {
assert_eq!(file, deserialized);
}
#[test]
fn to_and_from_json_no_meta() {
let file = VaultFile {
crypto: Crypto {
cipher: Cipher::Aes128Ctr(Aes128Ctr {
iv: "0155e3690be19fbfbecabcd440aa284b".into(),
}),
ciphertext: "4d6938a1f49b7782".into(),
kdf: Kdf::Pbkdf2(Pbkdf2 {
c: 1024,
dklen: 32,
prf: Prf::HmacSha256,
salt: "b6a9338a7ccd39288a86dba73bfecd9101b4f3db9c9830e7c76afdbd4f6872e5".into(),
}),
mac: "16381463ea11c6eb2239a9f339c2e780516d29d234ce30ac5f166f9080b5a262".into(),
},
meta: None,
};
let serialized = serde_json::to_string(&file).unwrap();
let deserialized = serde_json::from_str(&serialized).unwrap();
assert_eq!(file, deserialized);
}
}

18
hw/Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
description = "Hardware wallet support."
homepage = "http://parity.io"
license = "GPL-3.0"
name = "hardware-wallet"
version = "1.6.0"
authors = ["Parity Technologies <admin@parity.io>"]
[dependencies]
log = "0.3"
parking_lot = "0.3"
hidapi = { git = "https://github.com/ethcore/hidapi-rs" }
libusb = { git = "https://github.com/ethcore/libusb-rs" }
ethkey = { path = "../ethkey" }
ethcore-bigint = { path = "../util/bigint" }
[dev-dependencies]
rustc-serialize = "0.3"

364
hw/src/ledger.rs Normal file

File diff suppressed because one or more lines are too long

183
hw/src/lib.rs Normal file
View File

@ -0,0 +1,183 @@
// Copyright 2015-2017 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/>.
//! Hardware wallet management.
extern crate parking_lot;
extern crate hidapi;
extern crate libusb;
extern crate ethkey;
extern crate ethcore_bigint;
#[macro_use] extern crate log;
#[cfg(test)] extern crate rustc_serialize;
mod ledger;
use std::fmt;
use std::thread;
use std::sync::atomic;
use std::sync::{Arc, Weak};
use std::sync::atomic::AtomicBool;
use std::time::Duration;
use parking_lot::Mutex;
use ethkey::{Address, Signature};
pub use ledger::KeyPath;
/// Hardware waller error.
#[derive(Debug)]
pub enum Error {
/// Ledger device error.
LedgerDevice(ledger::Error),
/// USB error.
Usb(libusb::Error),
/// Hardware wallet not found for specified key.
KeyNotFound,
}
/// Hardware waller information.
#[derive(Debug, Clone)]
pub struct WalletInfo {
/// Wallet device name.
pub name: String,
/// Wallet device manufacturer.
pub manufacturer: String,
/// Wallet device serial number.
pub serial: String,
/// Ethereum address.
pub address: Address,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
Error::KeyNotFound => write!(f, "Key not found for given address."),
Error::LedgerDevice(ref e) => write!(f, "{}", e),
Error::Usb(ref e) => write!(f, "{}", e),
}
}
}
impl From<ledger::Error> for Error {
fn from(err: ledger::Error) -> Error {
match err {
ledger::Error::KeyNotFound => Error::KeyNotFound,
_ => Error::LedgerDevice(err),
}
}
}
impl From<libusb::Error> for Error {
fn from(err: libusb::Error) -> Error {
Error::Usb(err)
}
}
/// Hardware wallet management interface.
pub struct HardwareWalletManager {
update_thread: Option<thread::JoinHandle<()>>,
exiting: Arc<AtomicBool>,
ledger: Arc<Mutex<ledger::Manager>>,
}
struct EventHandler {
ledger: Weak<Mutex<ledger::Manager>>,
}
impl libusb::Hotplug for EventHandler {
fn device_arrived(&mut self, _device: libusb::Device) {
debug!("USB Device arrived");
if let Some(l) = self.ledger.upgrade() {
for _ in 0..10 {
// The device might not be visible right away. Try a few times.
if l.lock().update_devices().unwrap_or_else(|e| {
debug!("Error enumerating Ledger devices: {}", e);
0
}) > 0 {
break;
}
thread::sleep(Duration::from_millis(200));
}
}
}
fn device_left(&mut self, _device: libusb::Device) {
debug!("USB Device lost");
if let Some(l) = self.ledger.upgrade() {
if let Err(e) = l.lock().update_devices() {
debug!("Error enumerating Ledger devices: {}", e);
}
}
}
}
impl HardwareWalletManager {
pub fn new() -> Result<HardwareWalletManager, Error> {
let usb_context = Arc::new(libusb::Context::new()?);
let ledger = Arc::new(Mutex::new(ledger::Manager::new()?));
usb_context.register_callback(None, None, None, Box::new(EventHandler { ledger: Arc::downgrade(&ledger) }))?;
let exiting = Arc::new(AtomicBool::new(false));
let thread_exiting = exiting.clone();
let l = ledger.clone();
let thread = thread::Builder::new().name("hw_wallet".to_string()).spawn(move || {
if let Err(e) = l.lock().update_devices() {
debug!("Error updating ledger devices: {}", e);
}
loop {
usb_context.handle_events(Some(Duration::from_millis(500))).unwrap_or_else(|e| debug!("Error processing USB events: {}", e));
if thread_exiting.load(atomic::Ordering::Acquire) {
break;
}
}
}).ok();
Ok(HardwareWalletManager {
update_thread: thread,
exiting: exiting,
ledger: ledger,
})
}
/// Select key derivation path for a chain.
pub fn set_key_path(&self, key_path: KeyPath) {
self.ledger.lock().set_key_path(key_path);
}
/// List connected wallets. This only returns wallets that are ready to be used.
pub fn list_wallets(&self) -> Vec<WalletInfo> {
self.ledger.lock().list_devices()
}
/// Get connected wallet info.
pub fn wallet_info(&self, address: &Address) -> Option<WalletInfo> {
self.ledger.lock().device_info(address)
}
/// Sign transaction data with wallet managing `address`.
pub fn sign_transaction(&self, address: &Address, data: &[u8]) -> Result<Signature, Error> {
Ok(self.ledger.lock().sign_transaction(address, data)?)
}
}
impl Drop for HardwareWalletManager {
fn drop(&mut self) {
self.exiting.store(true, atomic::Ordering::Release);
if let Some(thread) = self.update_thread.take() {
thread.thread().unpark();
thread.join().ok();
}
}
}

View File

@ -4,11 +4,11 @@
"stage-0", "react"
],
"plugins": [
"transform-runtime",
"transform-decorators-legacy",
"transform-class-properties",
"transform-object-rest-spread",
"lodash"
"lodash",
"recharts"
],
"retainLines": true,
"env": {
@ -25,6 +25,7 @@
},
"test": {
"plugins": [
"transform-runtime",
[ "babel-plugin-webpack-alias", { "config": "webpack/test.js" } ]
]
}

View File

@ -1,6 +1,6 @@
{
"name": "parity.js",
"version": "0.3.72",
"version": "0.3.77",
"main": "release/index.js",
"jsnext:main": "src/index.js",
"author": "Parity Team <admin@parity.io>",
@ -25,18 +25,20 @@
"Promise"
],
"scripts": {
"build": "npm run build:lib && npm run build:dll && npm run build:app",
"build": "npm run build:lib && npm run build:dll && npm run build:app && npm run build:embed",
"build:app": "webpack --config webpack/app",
"build:lib": "webpack --config webpack/libraries",
"build:dll": "webpack --config webpack/vendor",
"build:markdown": "babel-node ./scripts/build-rpc-markdown.js",
"build:json": "babel-node ./scripts/build-rpc-json.js",
"ci:build": "npm run ci:build:lib && npm run ci:build:dll && npm run ci:build:app",
"build:embed": "EMBED=1 node webpack/embed",
"ci:build": "npm run ci:build:lib && npm run ci:build:dll && npm run ci:build:app && npm run ci:build:embed",
"ci:build:app": "NODE_ENV=production webpack --config webpack/app",
"ci:build:lib": "NODE_ENV=production webpack --config webpack/libraries",
"ci:build:dll": "NODE_ENV=production webpack --config webpack/vendor",
"ci:build:npm": "NODE_ENV=production webpack --config webpack/npm",
"ci:build:jsonrpc": "babel-node ./scripts/build-rpc-json.js --output .npmjs/jsonrpc",
"ci:build:embed": "NODE_ENV=production EMBED=1 node webpack/embed",
"start": "npm install && npm run build:lib && npm run build:dll && npm run start:app",
"start:app": "node webpack/dev.server",
"clean": "rm -rf ./.build ./.coverage ./.happypack ./.npmjs ./build",
@ -53,27 +55,28 @@
"prepush": "npm run lint:cached"
},
"devDependencies": {
"babel-cli": "6.18.0",
"babel-core": "6.21.0",
"babel-cli": "6.22.2",
"babel-core": "6.22.1",
"babel-eslint": "7.1.1",
"babel-loader": "6.2.10",
"babel-plugin-lodash": "3.2.11",
"babel-plugin-react-intl": "2.2.0",
"babel-plugin-transform-class-properties": "6.19.0",
"babel-plugin-react-intl": "2.3.1",
"babel-plugin-recharts": "1.1.0",
"babel-plugin-transform-class-properties": "6.22.0",
"babel-plugin-transform-decorators-legacy": "1.3.4",
"babel-plugin-transform-object-rest-spread": "6.20.2",
"babel-plugin-transform-react-remove-prop-types": "0.2.11",
"babel-plugin-transform-runtime": "6.15.0",
"babel-plugin-transform-object-rest-spread": "6.22.0",
"babel-plugin-transform-react-remove-prop-types": "0.3.0",
"babel-plugin-transform-runtime": "6.22.0",
"babel-plugin-webpack-alias": "2.1.2",
"babel-polyfill": "6.20.0",
"babel-preset-env": "1.1.4",
"babel-preset-es2015": "6.18.0",
"babel-preset-es2016": "6.16.0",
"babel-preset-es2017": "6.16.0",
"babel-preset-react": "6.16.0",
"babel-preset-stage-0": "6.16.0",
"babel-register": "6.18.0",
"babel-runtime": "6.20.0",
"babel-polyfill": "6.22.0",
"babel-preset-env": "1.1.8",
"babel-preset-es2015": "6.22.0",
"babel-preset-es2016": "6.22.0",
"babel-preset-es2017": "6.22.0",
"babel-preset-react": "6.22.0",
"babel-preset-stage-0": "6.22.0",
"babel-register": "6.22.0",
"babel-runtime": "6.22.0",
"chai": "3.5.0",
"chai-as-promised": "6.0.0",
"chai-enzyme": "0.6.1",
@ -132,7 +135,7 @@
"stylelint": "7.7.0",
"stylelint-config-standard": "15.0.1",
"url-loader": "0.5.7",
"webpack": "2.2.0-rc.2",
"webpack": "2.2.1",
"webpack-dev-middleware": "1.9.0",
"webpack-error-notification": "0.1.6",
"webpack-hot-middleware": "2.14.0",

View File

@ -17,6 +17,7 @@
import BigNumber from 'bignumber.js';
import { toChecksumAddress } from '../../abi/util/address';
import { isString } from '../util/types';
export function outAccountInfo (infos) {
return Object
@ -344,3 +345,17 @@ export function outTraceReplay (trace) {
return trace;
}
export function outVaultMeta (meta) {
if (isString(meta)) {
try {
const obj = JSON.parse(meta);
return obj;
} catch (error) {
return {};
}
}
return meta || {};
}

View File

@ -16,7 +16,7 @@
import BigNumber from 'bignumber.js';
import { outBlock, outAccountInfo, outAddress, outChainStatus, outDate, outHistogram, outNumber, outPeer, outPeers, outReceipt, outSyncing, outTransaction, outTrace } from './output';
import { outBlock, outAccountInfo, outAddress, outChainStatus, outDate, outHistogram, outNumber, outPeer, outPeers, outReceipt, outSyncing, outTransaction, outTrace, outVaultMeta } from './output';
import { isAddress, isBigNumber, isInstanceOf } from '../../../test/types';
describe('api/format/output', () => {
@ -455,4 +455,22 @@ describe('api/format/output', () => {
expect(formatted.transactionPosition.toNumber()).to.equal(11);
});
});
describe('outVaultMeta', () => {
it('returns an exmpt object on null', () => {
expect(outVaultMeta(null)).to.deep.equal({});
});
it('returns the original value if not string', () => {
expect(outVaultMeta({ test: 123 })).to.deep.equal({ test: 123 });
});
it('returns an object from JSON string', () => {
expect(outVaultMeta('{"test":123}')).to.deep.equal({ test: 123 });
});
it('returns an empty object on invalid JSON', () => {
expect(outVaultMeta('{"test"}')).to.deep.equal({});
});
});
});

View File

@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { inAddress, inAddresses, inData, inHex, inNumber16, inOptions, inBlockNumber } from '../../format/input';
import { outAccountInfo, outAddress, outAddresses, outChainStatus, outHistogram, outNumber, outPeers, outTransaction } from '../../format/output';
import { outAccountInfo, outAddress, outAddresses, outChainStatus, outHistogram, outNumber, outPeers, outTransaction, outVaultMeta } from '../../format/output';
export default class Parity {
constructor (transport) {
@ -55,11 +55,26 @@ export default class Parity {
.execute('parity_changePassword', inAddress(account), password, newPassword);
}
changeVault (account, vaultName) {
return this._transport
.execute('parity_changeVault', inAddress(account), vaultName);
}
changeVaultPassword (vaultName, password) {
return this._transport
.execute('parity_changeVaultPassword', vaultName, password);
}
checkRequest (requestId) {
return this._transport
.execute('parity_checkRequest', inNumber16(requestId));
}
closeVault (vaultName) {
return this._transport
.execute('parity_closeVault', vaultName);
}
consensusCapability () {
return this._transport
.execute('parity_consensusCapability');
@ -167,6 +182,12 @@ export default class Parity {
.then((addresses) => addresses ? addresses.map(outAddress) : null);
}
getVaultMeta (vaultName) {
return this._transport
.execute('parity_getVaultMeta', vaultName)
.then(outVaultMeta);
}
hashContent (url) {
return this._transport
.execute('parity_hashContent', url);
@ -189,6 +210,16 @@ export default class Parity {
.then((accounts) => (accounts || []).map(outAddress));
}
listOpenedVaults () {
return this._transport
.execute('parity_listOpenedVaults');
}
listVaults () {
return this._transport
.execute('parity_listVaults');
}
listRecentDapps () {
return this._transport
.execute('parity_listRecentDapps');
@ -275,6 +306,11 @@ export default class Parity {
.then(outAddress);
}
newVault (vaultName, password) {
return this._transport
.execute('parity_newVault', vaultName, password);
}
nextNonce (account) {
return this._transport
.execute('parity_nextNonce', inAddress(account))
@ -286,6 +322,11 @@ export default class Parity {
.execute('parity_nodeName');
}
openVault (vaultName, password) {
return this._transport
.execute('parity_openVault', vaultName, password);
}
pendingTransactions () {
return this._transport
.execute('parity_pendingTransactions')
@ -399,6 +440,11 @@ export default class Parity {
.execute('parity_setTransactionsLimit', inNumber16(quantity));
}
setVaultMeta (vaultName, meta) {
return this._transport
.execute('parity_setVaultMeta', vaultName, JSON.stringify(meta));
}
signerPort () {
return this._transport
.execute('parity_signerPort')

View File

@ -104,11 +104,14 @@ export default class Personal {
}
switch (data.method) {
case 'parity_closeVault':
case 'parity_openVault':
case 'parity_killAccount':
case 'parity_importGethAccounts':
case 'personal_newAccount':
case 'parity_newAccountFromPhrase':
case 'parity_newAccountFromWallet':
case 'personal_newAccount':
this._defaultAccount(true);
this._listAccounts();
this._accountsInfo();
return;
@ -116,6 +119,7 @@ export default class Personal {
case 'parity_removeAddress':
case 'parity_setAccountName':
case 'parity_setAccountMeta':
case 'parity_changeVault':
this._accountsInfo();
return;

View File

@ -259,7 +259,7 @@ export class LocalTransaction extends BaseTransaction {
to: transaction.to,
nonce: transaction.nonce,
value: transaction.value,
data: transaction.data,
data: transaction.input,
gasPrice, gas
};

View File

@ -105,11 +105,14 @@ export default class InputText extends Component {
const { validationType, contract } = this.props;
const validation = validate(value, validationType, contract);
if (validation instanceof Promise) {
const loadingTimeout = setTimeout(() => {
this.setState({ disabled: true, loading: true });
}, 50);
return Promise.resolve(validation)
.then((validation) => {
clearTimeout(loadingTimeout);
return validation
.then(validation => {
this.setValidation({
...validation,
disabled: false,
@ -120,9 +123,6 @@ export default class InputText extends Component {
});
}
this.setValidation(validation);
}
onKeyDown = (event) => {
if (!this.props.onEnter) {
return;

View File

@ -49,7 +49,7 @@
}
.token-container {
flex: 1;
flex: 1 1 auto;
}
.full-width .token-container {

View File

@ -17,7 +17,7 @@
const DEFAULT_LOCALE = 'en';
const DEFAULT_LOCALES = process.env.NODE_ENV === 'production'
? ['en']
: ['en', 'de'];
: ['en', 'de', 'nl'];
const LS_STORE_KEY = '_parity::locale';
export {

View File

@ -16,5 +16,6 @@
export default {
de: 'Deutsch',
en: 'English'
en: 'English',
nl: 'Nederlands'
};

21
js/src/i18n/nl/index.js Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2015-2017 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 settings from './settings';
export default {
settings
};

View File

@ -0,0 +1,63 @@
// Copyright 2015-2017 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 default {
label: 'Instellingen',
background: {
label: 'Achtergrond'
},
parity: {
label: 'Parity'
},
proxy: {
label: 'Proxy'
},
views: {
label: 'Weergaven',
accounts: {
label: 'Accounts'
},
addresses: {
label: 'Adresboek'
},
apps: {
label: 'Applicaties'
},
contracts: {
label: 'Contracten'
},
status: {
label: 'Status'
},
signer: {
label: 'Signer'
},
settings: {
label: 'Instellingen'
}
}
};

View File

@ -17,11 +17,12 @@
import { Address, Data, Hash, Quantity, BlockNumber, TransactionRequest } from '../types';
import { fromDecimal, withComment, Dummy } from '../helpers';
const SECTION_MINING = 'Block Authoring (aka "mining")';
const SECTION_DEV = 'Development';
const SECTION_NODE = 'Node Settings';
const SECTION_NET = 'Network Information';
const SECTION_ACCOUNTS = 'Accounts (read-only) and Signatures';
const SECTION_DEV = 'Development';
const SECTION_MINING = 'Block Authoring (aka "mining")';
const SECTION_NET = 'Network Information';
const SECTION_NODE = 'Node Settings';
const SECTION_VAULT = 'Account Vaults';
const SUBDOC_SET = 'set';
const SUBDOC_ACCOUNTS = 'accounts';
@ -151,6 +152,67 @@ export default {
}
},
changeVault: {
section: SECTION_VAULT,
desc: 'Changes the current valut for the account',
params: [
{
type: Address,
desc: 'Account address',
example: '0x63Cf90D3f0410092FC0fca41846f596223979195'
},
{
type: String,
desc: 'Vault name',
example: 'StrongVault'
}
],
returns: {
type: Boolean,
desc: 'True on success',
example: true
}
},
changeVaultPassword: {
section: SECTION_VAULT,
desc: 'Changes the password for any given vault',
params: [
{
type: String,
desc: 'Vault name',
example: 'StrongVault'
},
{
type: String,
desc: 'New Password',
example: 'p@55w0rd'
}
],
returns: {
type: Boolean,
desc: 'True on success',
example: true
}
},
closeVault: {
section: SECTION_VAULT,
desc: 'Closes a vault with the given name',
params: [
{
type: String,
desc: 'Vault name',
example: 'StrongVault'
}
],
returns: {
type: Boolean,
desc: 'True on success',
example: true
}
},
consensusCapability: {
desc: 'Returns information on current consensus capability.',
params: [],
@ -314,6 +376,43 @@ export default {
}
},
getVaultMeta: {
section: SECTION_VAULT,
desc: 'Returns the metadata for a specific vault',
params: [
{
type: String,
desc: 'Vault name',
example: 'StrongVault'
}
],
returns: {
type: String,
desc: 'The associated JSON metadata for this vault',
example: '{"passwordHint":"something"}'
}
},
listOpenedVaults: {
desc: 'Returns a list of all opened vaults',
params: [],
returns: {
type: Array,
desc: 'Names of all opened vaults',
example: "['Personal']"
}
},
listVaults: {
desc: 'Returns a list of all available vaults',
params: [],
returns: {
type: Array,
desc: 'Names of all available vaults',
example: "['Personal','Work']"
}
},
localTransactions: {
desc: 'Returns an object of current and past local transactions.',
params: [],
@ -430,6 +529,28 @@ export default {
}
},
newVault: {
section: SECTION_VAULT,
desc: 'Creates a new vault with the given name & password',
params: [
{
type: String,
desc: 'Vault name',
example: 'StrongVault'
},
{
type: String,
desc: 'Password',
example: 'p@55w0rd'
}
],
returns: {
type: Boolean,
desc: 'True on success',
example: true
}
},
nextNonce: {
section: SECTION_NET,
desc: 'Returns next available nonce for transaction from given account. Includes pending block and transaction queue.',
@ -458,6 +579,28 @@ export default {
}
},
openVault: {
section: SECTION_VAULT,
desc: 'Opens a vault with the given name & password',
params: [
{
type: String,
desc: 'Vault name',
example: 'StrongVault'
},
{
type: String,
desc: 'Password',
example: 'p@55w0rd'
}
],
returns: {
type: Boolean,
desc: 'True on success',
example: true
}
},
pendingTransactions: {
section: SECTION_NET,
desc: 'Returns a list of transactions currently in the queue.',
@ -594,6 +737,28 @@ export default {
}
},
setVaultMeta: {
section: SECTION_VAULT,
desc: 'Sets the metadata for a specific vault',
params: [
{
type: String,
desc: 'Vault name',
example: 'StrongVault'
},
{
type: String,
desc: 'The metadata as a JSON string',
example: '{"passwordHint":"something"}'
}
],
returns: {
type: Boolean,
desc: 'The boolean call result, true on success',
example: true
}
},
signerPort: {
section: SECTION_NODE,
desc: 'Returns the port the signer is running on, error if not enabled',

View File

@ -31,4 +31,4 @@ if (isNode) {
import Etherscan from './3rdparty/etherscan';
module.exports = Etherscan;
export default Etherscan;

View File

@ -16,4 +16,4 @@
import JsonRpc from './jsonrpc';
module.exports = JsonRpc;
export default JsonRpc;

View File

@ -32,4 +32,4 @@ if (isNode) {
import Abi from './abi';
import Api from './api';
module.exports = { Api, Abi };
export { Api, Abi };

View File

@ -31,4 +31,4 @@ if (isNode) {
import ShapeShift from './3rdparty/shapeshift';
module.exports = ShapeShift;
export default ShapeShift;

View File

@ -20,7 +20,8 @@ import { FormattedMessage } from 'react-intl';
import { IconButton } from 'material-ui';
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
import { Form, Input, IdentityIcon, PasswordStrength } from '~/ui';
import { Form, Input, IdentityIcon } from '~/ui';
import PasswordStrength from '~/ui/Form/PasswordStrength';
import { RefreshIcon } from '~/ui/Icons';
import styles from '../createAccount.css';

View File

@ -18,7 +18,8 @@ import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Form, Input, PasswordStrength } from '~/ui';
import { Form, Input } from '~/ui';
import PasswordStrength from '~/ui/Form/PasswordStrength';
import styles from '../createAccount.css';

View File

@ -19,7 +19,8 @@ import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Checkbox } from 'material-ui';
import { Form, Input, PasswordStrength } from '~/ui';
import { Form, Input } from '~/ui';
import PasswordStrength from '~/ui/Form/PasswordStrength';
import styles from '../createAccount.css';

View File

@ -20,7 +20,8 @@ import moment from 'moment';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Button, Modal, Editor } from '~/ui';
import { Button, Modal } from '~/ui';
import Editor from '~/ui/Editor';
import { CancelIcon, CheckIcon, DeleteIcon } from '~/ui/Icons';
import styles from './loadContract.css';

View File

@ -23,7 +23,8 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { newError, openSnackbar } from '~/redux/actions';
import { Button, Modal, IdentityName, IdentityIcon, PasswordStrength } from '~/ui';
import { Button, Modal, IdentityName, IdentityIcon } from '~/ui';
import PasswordStrength from '~/ui/Form/PasswordStrength';
import Form, { Input } from '~/ui/Form';
import { CancelIcon, CheckIcon, SendIcon } from '~/ui/Icons';

View File

@ -19,7 +19,8 @@ import React, { Component, PropTypes } from 'react';
import SaveIcon from 'material-ui/svg-icons/content/save';
import ContentClear from 'material-ui/svg-icons/content/clear';
import { Button, Modal, Editor, Form, Input } from '~/ui';
import { Button, Modal, Form, Input } from '~/ui';
import Editor from '~/ui/Editor';
import { ERRORS, validateName } from '~/util/validation';
import styles from './saveContract.css';

View File

@ -14,15 +14,13 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import { Checkbox, MenuItem } from 'material-ui';
import { isEqual } from 'lodash';
import { Checkbox } from 'material-ui';
import Form, { Input, InputAddressSelect, AddressSelect, Select } from '~/ui/Form';
import Form, { Input, InputAddressSelect, AddressSelect } from '~/ui/Form';
import { nullableProptype } from '~/util/proptypes';
import imageUnknown from '../../../../assets/images/contracts/unknown-64x64.png';
import TokenSelect from './tokenSelect';
import styles from '../transfer.css';
const CHECK_STYLE = {
@ -31,110 +29,12 @@ const CHECK_STYLE = {
left: '1em'
};
class TokenSelect extends Component {
static contextTypes = {
api: PropTypes.object
}
static propTypes = {
onChange: PropTypes.func.isRequired,
balance: PropTypes.object.isRequired,
images: PropTypes.object.isRequired,
tag: PropTypes.string.isRequired
};
componentWillMount () {
this.computeTokens();
}
componentWillReceiveProps (nextProps) {
const prevTokens = this.props.balance.tokens.map((t) => `${t.token.tag}_${t.value.toNumber()}`);
const nextTokens = nextProps.balance.tokens.map((t) => `${t.token.tag}_${t.value.toNumber()}`);
if (!isEqual(prevTokens, nextTokens)) {
this.computeTokens(nextProps);
}
}
computeTokens (props = this.props) {
const { api } = this.context;
const { balance, images } = this.props;
const items = balance.tokens
.filter((token, index) => !index || token.value.gt(0))
.map((balance, index) => {
const token = balance.token;
const isEth = index === 0;
let imagesrc = token.image;
if (!imagesrc) {
imagesrc =
images[token.address]
? `${api.dappsUrl}${images[token.address]}`
: imageUnknown;
}
let value = 0;
if (isEth) {
value = api.util.fromWei(balance.value).toFormat(3);
} else {
const format = balance.token.format || 1;
const decimals = format === 1 ? 0 : Math.min(3, Math.floor(format / 10));
value = new BigNumber(balance.value).div(format).toFormat(decimals);
}
const label = (
<div className={ styles.token }>
<img src={ imagesrc } />
<div className={ styles.tokenname }>
{ token.name }
</div>
<div className={ styles.tokenbalance }>
{ value }<small> { token.tag }</small>
</div>
</div>
);
return (
<MenuItem
key={ `${index}_${token.tag}` }
value={ token.tag }
label={ label }
>
{ label }
</MenuItem>
);
});
this.setState({ items });
}
render () {
const { tag, onChange } = this.props;
const { items } = this.state;
return (
<Select
className={ styles.tokenSelect }
label='type of token transfer'
hint='type of token to transfer'
value={ tag }
onChange={ onChange }
>
{ items }
</Select>
);
}
}
export default class Details extends Component {
static propTypes = {
address: PropTypes.string,
balance: PropTypes.object,
all: PropTypes.bool,
extras: PropTypes.bool,
images: PropTypes.object.isRequired,
sender: PropTypes.string,
senderError: PropTypes.string,
sendersBalances: PropTypes.object,
@ -249,12 +149,11 @@ export default class Details extends Component {
}
renderTokenSelect () {
const { balance, images, tag } = this.props;
const { balance, tag } = this.props;
return (
<TokenSelect
balance={ balance }
images={ images }
tag={ tag }
onChange={ this.onChangeToken }
/>

View File

@ -0,0 +1,114 @@
// Copyright 2015-2017 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 React, { Component, PropTypes } from 'react';
import { MenuItem } from 'material-ui';
import { isEqual } from 'lodash';
import { Select } from '~/ui/Form';
import TokenImage from '~/ui/TokenImage';
import styles from '../transfer.css';
export default class TokenSelect extends Component {
static contextTypes = {
api: PropTypes.object
};
static propTypes = {
onChange: PropTypes.func.isRequired,
balance: PropTypes.object.isRequired,
tag: PropTypes.string.isRequired
};
componentWillMount () {
this.computeTokens();
}
componentWillReceiveProps (nextProps) {
const prevTokens = this.props.balance.tokens.map((t) => `${t.token.tag}_${t.value.toNumber()}`);
const nextTokens = nextProps.balance.tokens.map((t) => `${t.token.tag}_${t.value.toNumber()}`);
if (!isEqual(prevTokens, nextTokens)) {
this.computeTokens(nextProps);
}
}
computeTokens (props = this.props) {
const { api } = this.context;
const { balance } = this.props;
const items = balance.tokens
.filter((token, index) => !index || token.value.gt(0))
.map((balance, index) => {
const token = balance.token;
const isEth = index === 0;
let value = 0;
if (isEth) {
value = api.util.fromWei(balance.value).toFormat(3);
} else {
const format = balance.token.format || 1;
const decimals = format === 1 ? 0 : Math.min(3, Math.floor(format / 10));
value = new BigNumber(balance.value).div(format).toFormat(decimals);
}
const label = (
<div className={ styles.token }>
<TokenImage token={ token } />
<div className={ styles.tokenname }>
{ token.name }
</div>
<div className={ styles.tokenbalance }>
{ value }<small> { token.tag }</small>
</div>
</div>
);
return (
<MenuItem
key={ `${index}_${token.tag}` }
value={ token.tag }
label={ label }
>
{ label }
</MenuItem>
);
});
this.setState({ items });
}
render () {
const { tag, onChange } = this.props;
const { items } = this.state;
return (
<Select
className={ styles.tokenSelect }
label='type of token transfer'
hint='type of token to transfer'
value={ tag }
onChange={ onChange }
>
{ items }
</Select>
);
}
}

View File

@ -44,7 +44,6 @@ class Transfer extends Component {
static propTypes = {
newError: PropTypes.func.isRequired,
gasLimit: PropTypes.object.isRequired,
images: PropTypes.object.isRequired,
senders: nullableProptype(PropTypes.object),
sendersBalances: nullableProptype(PropTypes.object),
@ -174,7 +173,7 @@ class Transfer extends Component {
}
renderDetailsPage () {
const { account, balance, images, senders } = this.props;
const { account, balance, senders } = this.props;
const { recipient, recipientError, sender, senderError, sendersBalances } = this.store;
const { valueAll, extras, tag, total, totalError, value, valueError } = this.store;
@ -184,7 +183,6 @@ class Transfer extends Component {
all={ valueAll }
balance={ balance }
extras={ extras }
images={ images }
onChange={ this.store.onUpdateDetails }
recipient={ recipient }
recipientError={ recipientError }

View File

@ -0,0 +1,23 @@
// Copyright 2015-2017 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/>.
if (!process.env.EMBED) {
const setupWorker = require('./worker').setupWorker;
module.exports = { setupWorker };
} else {
module.exports = { setupWorker: () => {} };
}

View File

@ -20,7 +20,7 @@ import initMiddleware from './middleware';
import initReducers from './reducers';
import { load as loadWallet } from './providers/walletActions';
import { setupWorker } from './providers/worker';
import { setupWorker } from './providers/workerWrapper';
import {
Balances as BalancesProvider,

View File

@ -65,7 +65,7 @@ describe('ui/AccountCard', () => {
let balance;
beforeEach(() => {
balance = component.find('Connect(Balance)');
balance = component.find('Balance');
});
it('renders the balance', () => {

View File

@ -14,4 +14,9 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export Export from './Export';
export Import from './Import';
export Search from './Search';
export Sort from './Sort';
export default from './actionbar';

View File

@ -17,13 +17,12 @@
import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import unknownImage from '~/../assets/images/contracts/unknown-64x64.png';
import TokenImage from '~/ui/TokenImage';
import styles from './balance.css';
class Balance extends Component {
export default class Balance extends Component {
static contextTypes = {
api: PropTypes.object
};
@ -31,7 +30,6 @@ class Balance extends Component {
static propTypes = {
balance: PropTypes.object,
className: PropTypes.string,
images: PropTypes.object.isRequired,
showOnlyEth: PropTypes.bool,
showZeroValues: PropTypes.bool
};
@ -43,7 +41,7 @@ class Balance extends Component {
render () {
const { api } = this.context;
const { balance, className, images, showZeroValues, showOnlyEth } = this.props;
const { balance, className, showZeroValues, showOnlyEth } = this.props;
if (!balance || !balance.tokens) {
return null;
@ -79,26 +77,12 @@ class Balance extends Component {
value = api.util.fromWei(balance.value).toFormat(3);
}
const imageurl = token.image || images[token.address];
let imagesrc = unknownImage;
if (imageurl) {
const host = /^(\/)?api/.test(imageurl)
? api.dappsUrl
: '';
imagesrc = `${host}${imageurl}`;
}
return (
<div
className={ styles.balance }
key={ `${index}_${token.tag}` }
>
<img
src={ imagesrc }
alt={ token.name }
/>
<TokenImage token={ token } />
<div className={ styles.balanceValue }>
<span title={ value }> { value } </span>
</div>
@ -125,14 +109,3 @@ class Balance extends Component {
);
}
}
function mapStateToProps (state) {
const { images } = state;
return { images };
}
export default connect(
mapStateToProps,
null
)(Balance);

View File

@ -16,7 +16,6 @@
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import apiutil from '~/api/util';
@ -32,7 +31,6 @@ const BALANCE = {
let api;
let component;
let store;
function createApi () {
api = {
@ -43,36 +41,22 @@ function createApi () {
return api;
}
function createStore () {
store = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
images: {}
};
}
};
return store;
}
function render (props = {}) {
if (!props.balance) {
props.balance = BALANCE;
}
const api = createApi();
component = shallow(
<Balance
className='testClass'
{ ...props }
/>,
{
context: {
store: createStore()
context: { api }
}
}
).find('Balance').shallow({ context: { api: createApi() } });
);
return component;
}
@ -91,18 +75,18 @@ describe('ui/Balance', () => {
});
it('renders all the non-zero balances', () => {
expect(component.find('img')).to.have.length(2);
expect(component.find('Connect(TokenImage)')).to.have.length(2);
});
describe('render specifiers', () => {
it('renders only the single token with showOnlyEth', () => {
render({ showOnlyEth: true });
expect(component.find('img')).to.have.length(1);
expect(component.find('Connect(TokenImage)')).to.have.length(1);
});
it('renders all the tokens with showZeroValues', () => {
render({ showZeroValues: true });
expect(component.find('img')).to.have.length(3);
expect(component.find('Connect(TokenImage)')).to.have.length(3);
});
it('shows ETH with zero value with showOnlyEth & showZeroValues', () => {
@ -116,7 +100,7 @@ describe('ui/Balance', () => {
]
}
});
expect(component.find('img')).to.have.length(1);
expect(component.find('Connect(TokenImage)')).to.have.length(1);
});
});
});

View File

@ -14,35 +14,19 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import AddressSelect from './AddressSelect';
import DappUrlInput from './DappUrlInput';
import FormWrap from './FormWrap';
import Input from './Input';
import InputAddress from './InputAddress';
import InputAddressSelect from './InputAddressSelect';
import InputChip from './InputChip';
import InputDate from './InputDate';
import InputInline from './InputInline';
import InputTime from './InputTime';
import Label from './Label';
import RadioButtons from './RadioButtons';
import Select from './Select';
import TypedInput from './TypedInput';
export AddressSelect from './AddressSelect';
export DappUrlInput from './DappUrlInput';
export FormWrap from './FormWrap';
export Input from './Input';
export InputAddress from './InputAddress';
export InputAddressSelect from './InputAddressSelect';
export InputChip from './InputChip';
export InputDate from './InputDate';
export InputInline from './InputInline';
export InputTime from './InputTime';
export Label from './Label';
export RadioButtons from './RadioButtons';
export Select from './Select';
export TypedInput from './TypedInput';
export default from './form';
export {
AddressSelect,
DappUrlInput,
FormWrap,
Input,
InputAddress,
InputAddressSelect,
InputChip,
InputDate,
InputInline,
InputTime,
Label,
RadioButtons,
Select,
TypedInput
};

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 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 default from './tokenImage';

View File

@ -0,0 +1,72 @@
// Copyright 2015-2017 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 React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import unknownImage from '~/../assets/images/contracts/unknown-64x64.png';
class TokenImage extends Component {
static contextTypes = {
api: PropTypes.object
};
static propTypes = {
image: PropTypes.string,
token: PropTypes.shape({
image: PropTypes.string,
address: PropTypes.string
}).isRequired
};
render () {
const { api } = this.context;
const { image, token } = this.props;
const imageurl = token.image || image;
let imagesrc = unknownImage;
if (imageurl) {
const host = /^(\/)?api/.test(imageurl)
? api.dappsUrl
: '';
imagesrc = `${host}${imageurl}`;
}
return (
<img
src={ imagesrc }
alt={ token.name }
/>
);
}
}
function mapStateToProps (iniState) {
const { images } = iniState;
return (_, props) => {
const { token } = props;
return { image: images[token.address] };
};
}
export default connect(
mapStateToProps,
null
)(TokenImage);

View File

@ -16,8 +16,10 @@
import moment from 'moment';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import { txLink, addressLink } from '~/3rdparty/etherscan/links';
import { txLink } from '~/3rdparty/etherscan/links';
import IdentityIcon from '../../IdentityIcon';
import IdentityName from '../../IdentityName';
@ -25,19 +27,20 @@ import MethodDecoding from '../../MethodDecoding';
import styles from '../txList.css';
export default class TxRow extends Component {
class TxRow extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
tx: PropTypes.object.isRequired,
accountAddresses: PropTypes.array.isRequired,
address: PropTypes.string.isRequired,
isTest: PropTypes.bool.isRequired,
tx: PropTypes.object.isRequired,
block: PropTypes.object,
historic: PropTypes.bool,
className: PropTypes.string
className: PropTypes.string,
historic: PropTypes.bool
};
static defaultProps = {
@ -77,22 +80,20 @@ export default class TxRow extends Component {
}
renderAddress (address) {
const { isTest } = this.props;
let esLink = null;
if (address) {
esLink = (
<a
href={ addressLink(address, isTest) }
target='_blank'
<Link
activeClassName={ styles.currentLink }
className={ styles.link }
to={ this.addressLink(address) }
>
<IdentityName
address={ address }
shorten
/>
</a>
</Link>
);
}
@ -138,4 +139,30 @@ export default class TxRow extends Component {
</td>
);
}
addressLink (address) {
const { accountAddresses } = this.props;
const isAccount = accountAddresses.includes(address);
if (isAccount) {
return `/accounts/${address}`;
}
return `/addresses/${address}`;
}
}
function mapStateToProps (initState) {
const { accounts } = initState.personal;
const accountAddresses = Object.keys(accounts);
return () => {
return { accountAddresses };
};
}
export default connect(
mapStateToProps,
null
)(TxRow);

View File

@ -25,13 +25,28 @@ import TxRow from './txRow';
const api = new Api({ execute: sinon.stub() });
const STORE = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
personal: {
accounts: {
'0x123': {}
}
}
};
}
};
function render (props) {
return shallow(
<TxRow
store={ STORE }
{ ...props }
/>,
{ context: { api } }
);
).find('TxRow').shallow({ context: { api } });
}
describe('ui/TxList/TxRow', () => {
@ -48,5 +63,37 @@ describe('ui/TxList/TxRow', () => {
expect(render({ address: '0x123', block, isTest: true, tx })).to.be.ok;
});
it('renders an account link', () => {
const block = {
timestamp: new Date()
};
const tx = {
blockNumber: new BigNumber(123),
hash: '0x123456789abcdef0123456789abcdef0123456789abcdef',
to: '0x123',
value: new BigNumber(1)
};
const element = render({ address: '0x123', block, isTest: true, tx });
expect(element.find('Link').prop('to')).to.equal('/accounts/0x123');
});
it('renders an address link', () => {
const block = {
timestamp: new Date()
};
const tx = {
blockNumber: new BigNumber(123),
hash: '0x123456789abcdef0123456789abcdef0123456789abcdef',
to: '0x456',
value: new BigNumber(1)
};
const element = render({ address: '0x123', block, isTest: true, tx });
expect(element.find('Link').prop('to')).to.equal('/addresses/0x456');
});
});
});

View File

@ -65,6 +65,11 @@
.link {
vertical-align: top;
&.currentLink {
color: white;
cursor: text;
}
}
.right {

View File

@ -14,118 +14,43 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import AccountCard from './AccountCard';
import Actionbar from './Actionbar';
import ActionbarExport from './Actionbar/Export';
import ActionbarImport from './Actionbar/Import';
import ActionbarSearch from './Actionbar/Search';
import ActionbarSort from './Actionbar/Sort';
import Badge from './Badge';
import Balance from './Balance';
import BlockStatus from './BlockStatus';
import Button from './Button';
import Certifications from './Certifications';
import ConfirmDialog from './ConfirmDialog';
import Container, { Title as ContainerTitle } from './Container';
import ContextProvider from './ContextProvider';
import CopyToClipboard from './CopyToClipboard';
import CurrencySymbol from './CurrencySymbol';
import DappCard from './DappCard';
import DappIcon from './DappIcon';
import Editor from './Editor';
import Errors from './Errors';
import Features, { FEATURES, FeaturesStore } from './Features';
import Form, { AddressSelect, DappUrlInput, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form';
import GasPriceEditor from './GasPriceEditor';
import GasPriceSelector from './GasPriceSelector';
import Icons from './Icons';
import IdentityIcon from './IdentityIcon';
import IdentityName from './IdentityName';
import LanguageSelector from './LanguageSelector';
import Loading from './Loading';
import MethodDecoding from './MethodDecoding';
import Modal, { Busy as BusyStep, Completed as CompletedStep } from './Modal';
import muiTheme from './Theme';
import Page from './Page';
import ParityBackground from './ParityBackground';
import PasswordStrength from './Form/PasswordStrength';
import Portal from './Portal';
import QrCode from './QrCode';
import SectionList from './SectionList';
import ShortenedHash from './ShortenedHash';
import SignerIcon from './SignerIcon';
import Tags from './Tags';
import Title from './Title';
import Tooltips, { Tooltip } from './Tooltips';
import TxHash from './TxHash';
import TxList from './TxList';
import Warning from './Warning';
export {
AccountCard,
Actionbar,
ActionbarExport,
ActionbarImport,
ActionbarSearch,
ActionbarSort,
AddressSelect,
Badge,
Balance,
BlockStatus,
Button,
Certifications,
ConfirmDialog,
Container,
ContainerTitle,
ContextProvider,
CopyToClipboard,
CurrencySymbol,
DappCard,
DappIcon,
DappUrlInput,
Editor,
Errors,
FEATURES,
Features,
FeaturesStore,
Form,
FormWrap,
GasPriceEditor,
GasPriceSelector,
Icons,
Input,
InputAddress,
InputAddressSelect,
InputChip,
InputDate,
InputInline,
InputTime,
IdentityIcon,
IdentityName,
Label,
LanguageSelector,
Loading,
MethodDecoding,
Modal,
BusyStep,
CompletedStep,
muiTheme,
Page,
ParityBackground,
PasswordStrength,
Portal,
QrCode,
RadioButtons,
Select,
ShortenedHash,
SectionList,
SignerIcon,
Tags,
Title,
Tooltip,
Tooltips,
TxHash,
TxList,
TypedInput,
Warning
};
export AccountCard from './AccountCard';
export Actionbar, { Export as ActionbarExport, Import as ActionbarImport, Search as ActionbarSearch, Sort as ActionbarSort } from './Actionbar';
export Badge from './Badge';
export Balance from './Balance';
export BlockStatus from './BlockStatus';
export Button from './Button';
export Certifications from './Certifications';
export ConfirmDialog from './ConfirmDialog';
export Container, { Title as ContainerTitle } from './Container';
export ContextProvider from './ContextProvider';
export CopyToClipboard from './CopyToClipboard';
export CurrencySymbol from './CurrencySymbol';
export DappCard from './DappCard';
export DappIcon from './DappIcon';
export Errors from './Errors';
export Features, { FEATURES, FeaturesStore } from './Features';
export Form, { AddressSelect, DappUrlInput, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form';
export GasPriceEditor from './GasPriceEditor';
export GasPriceSelector from './GasPriceSelector';
export Icons from './Icons';
export IdentityIcon from './IdentityIcon';
export IdentityName from './IdentityName';
export LanguageSelector from './LanguageSelector';
export Loading from './Loading';
export MethodDecoding from './MethodDecoding';
export Modal, { Busy as BusyStep, Completed as CompletedStep } from './Modal';
export muiTheme from './Theme';
export Page from './Page';
export ParityBackground from './ParityBackground';
export Portal from './Portal';
export QrCode from './QrCode';
export SectionList from './SectionList';
export ShortenedHash from './ShortenedHash';
export SignerIcon from './SignerIcon';
export Tags from './Tags';
export Title from './Title';
export Tooltips, { Tooltip } from './Tooltips';
export TxHash from './TxHash';
export TxList from './TxList';
export Warning from './Warning';

View File

@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import scrypt from 'scryptsy';
import * as Transaction from 'ethereumjs-tx';
import Transaction from 'ethereumjs-tx';
import { pbkdf2Sync } from 'crypto';
import { createDecipheriv } from 'browserify-aes';

View File

@ -73,7 +73,7 @@ describe('views/Account/Header', () => {
beforeEach(() => {
render({ balance: { balance: 'testing' } });
balance = component.find('Connect(Balance)');
balance = component.find('Balance');
});
it('renders', () => {

View File

@ -37,7 +37,6 @@ class Account extends Component {
static propTypes = {
fetchCertifiers: PropTypes.func.isRequired,
fetchCertifications: PropTypes.func.isRequired,
images: PropTypes.object.isRequired,
setVisibleAccounts: PropTypes.func.isRequired,
accounts: PropTypes.object,
@ -257,14 +256,13 @@ class Account extends Component {
return null;
}
const { balances, images } = this.props;
const { balances } = this.props;
return (
<Transfer
account={ account }
balance={ balance }
balances={ balances }
images={ images }
onClose={ this.store.toggleTransferDialog }
/>
);
@ -289,12 +287,10 @@ class Account extends Component {
function mapStateToProps (state) {
const { accounts } = state.personal;
const { balances } = state.balances;
const { images } = state;
return {
accounts,
balances,
images
balances
};
}

View File

@ -30,7 +30,8 @@ import { newError } from '~/redux/actions';
import { setVisibleAccounts } from '~/redux/providers/personalActions';
import { EditMeta, ExecuteContract } from '~/modals';
import { Actionbar, Button, Page, Modal, Editor } from '~/ui';
import { Actionbar, Button, Page, Modal } from '~/ui';
import Editor from '~/ui/Editor';
import Header from '../Account/Header';
import Delete from '../Address/Delete';

View File

@ -285,6 +285,7 @@ class ParityBar extends Component {
}
renderExpanded () {
const { externalLink } = this.props;
const { displayType } = this.state;
return (
@ -333,7 +334,7 @@ class ParityBar extends Component {
/>
)
: (
<Signer />
<Signer externalLink={ externalLink } />
)
}
</div>

View File

@ -15,16 +15,19 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import { addressLink } from '~/3rdparty/etherscan/links';
import styles from './accountLink.css';
export default class AccountLink extends Component {
class AccountLink extends Component {
static propTypes = {
isTest: PropTypes.bool.isRequired,
accountAddresses: PropTypes.array.isRequired,
address: PropTypes.string.isRequired,
className: PropTypes.string,
children: PropTypes.node
children: PropTypes.node,
externalLink: PropTypes.string.isRequired,
isTest: PropTypes.bool.isRequired
}
state = {
@ -32,20 +35,21 @@ export default class AccountLink extends Component {
};
componentWillMount () {
const { address, isTest } = this.props;
const { address, externalLink, isTest } = this.props;
this.updateLink(address, isTest);
this.updateLink(address, externalLink, isTest);
}
componentWillReceiveProps (nextProps) {
const { address, isTest } = nextProps;
const { address, externalLink, isTest } = nextProps;
this.updateLink(address, isTest);
this.updateLink(address, externalLink, isTest);
}
render () {
const { children, address, className } = this.props;
const { children, address, className, externalLink } = this.props;
if (externalLink) {
return (
<a
href={ this.state.link }
@ -57,11 +61,46 @@ export default class AccountLink extends Component {
);
}
updateLink (address, isTest) {
const link = addressLink(address, isTest);
return (
<Link
className={ `${styles.container} ${className}` }
to={ this.state.link }
>
{ children || address }
</Link>
);
}
updateLink (address, externalLink, isTest) {
const { accountAddresses } = this.props;
const isAccount = accountAddresses.includes(address);
let link = isAccount
? `/accounts/${address}`
: `/addresses/${address}`;
if (externalLink) {
const path = externalLink.replace(/\/+$/, '');
link = `${path}/#${link}`;
}
this.setState({
link
});
}
}
function mapStateToProps (initState) {
const { accounts } = initState.personal;
const accountAddresses = Object.keys(accounts);
return () => {
return { accountAddresses };
};
}
export default connect(
mapStateToProps,
null
)(AccountLink);

View File

@ -25,6 +25,7 @@ export default class Account extends Component {
static propTypes = {
className: PropTypes.string,
address: PropTypes.string.isRequired,
externalLink: PropTypes.string.isRequired,
isTest: PropTypes.bool.isRequired,
balance: PropTypes.object // eth BigNumber, not required since it mght take time to fetch
};
@ -51,12 +52,13 @@ export default class Account extends Component {
}
render () {
const { address, isTest, className } = this.props;
const { address, externalLink, isTest, className } = this.props;
return (
<div className={ `${styles.acc} ${className}` }>
<AccountLink
address={ address }
externalLink={ externalLink }
isTest={ isTest }
>
<IdentityIcon
@ -79,13 +81,14 @@ export default class Account extends Component {
}
renderName () {
const { address, isTest } = this.props;
const { address, externalLink, isTest } = this.props;
const name = <IdentityName address={ address } empty />;
if (!name) {
return (
<AccountLink
address={ address }
externalLink={ externalLink }
isTest={ isTest }
>
[{ this.shortAddress(address) }]
@ -96,6 +99,7 @@ export default class Account extends Component {
return (
<AccountLink
address={ address }
externalLink={ externalLink }
isTest={ isTest }
>
<span>

View File

@ -93,7 +93,9 @@ export default class SignRequest extends Component {
renderDetails () {
const { api } = this.context;
const { address, isTest, store, data } = this.props;
const balance = store.balances[address];
const { balances, externalLink } = store;
const balance = balances[address];
if (!balance) {
return <div />;
@ -105,6 +107,7 @@ export default class SignRequest extends Component {
<Account
address={ address }
balance={ balance }
externalLink={ externalLink }
isTest={ isTest }
/>
</div>

View File

@ -27,6 +27,7 @@ import styles from './transactionMainDetails.css';
export default class TransactionMainDetails extends Component {
static propTypes = {
children: PropTypes.node,
externalLink: PropTypes.string.isRequired,
from: PropTypes.string.isRequired,
fromBalance: PropTypes.object,
gasStore: PropTypes.object,
@ -50,7 +51,7 @@ export default class TransactionMainDetails extends Component {
}
render () {
const { children, from, fromBalance, gasStore, isTest, transaction } = this.props;
const { children, externalLink, from, fromBalance, gasStore, isTest, transaction } = this.props;
return (
<div className={ styles.transaction }>
@ -59,6 +60,7 @@ export default class TransactionMainDetails extends Component {
<Account
address={ from }
balance={ fromBalance }
externalLink={ externalLink }
isTest={ isTest }
/>
</div>

View File

@ -89,14 +89,16 @@ export default class TransactionPending extends Component {
renderTransaction () {
const { className, focus, id, isSending, isTest, store, transaction } = this.props;
const { totalValue } = this.state;
const { balances, externalLink } = store;
const { from, value } = transaction;
const fromBalance = store.balances[from];
const fromBalance = balances[from];
return (
<div className={ `${styles.container} ${className}` }>
<TransactionMainDetails
className={ styles.transactionDetails }
externalLink={ externalLink }
from={ from }
fromBalance={ fromBalance }
gasStore={ this.gasStore }

View File

@ -37,6 +37,7 @@ class Embedded extends Component {
startConfirmRequest: PropTypes.func.isRequired,
startRejectRequest: PropTypes.func.isRequired
}).isRequired,
externalLink: PropTypes.string,
gasLimit: PropTypes.object.isRequired,
isTest: PropTypes.bool.isRequired,
signer: PropTypes.shape({
@ -45,7 +46,7 @@ class Embedded extends Component {
}).isRequired
};
store = new Store(this.context.api);
store = new Store(this.context.api, false, this.props.externalLink);
render () {
return (

View File

@ -17,13 +17,16 @@
import { isEqual } from 'lodash';
import { action, observable } from 'mobx';
export default class Store {
export default class SignerStore {
@observable balances = {};
@observable localHashes = [];
constructor (api, withLocalTransactions = false) {
externalLink = '';
constructor (api, withLocalTransactions = false, externalLink = '') {
this._api = api;
this._timeoutId = 0;
this.externalLink = externalLink;
if (withLocalTransactions) {
this.fetchLocalTransactions();

View File

@ -66,7 +66,6 @@ class Wallet extends Component {
static propTypes = {
address: PropTypes.string.isRequired,
balance: nullableProptype(PropTypes.object.isRequired),
images: PropTypes.object.isRequired,
isTest: PropTypes.bool.isRequired,
owned: PropTypes.bool.isRequired,
setVisibleAccounts: PropTypes.func.isRequired,
@ -315,13 +314,12 @@ class Wallet extends Component {
return null;
}
const { walletAccount, balance, images } = this.props;
const { walletAccount, balance } = this.props;
return (
<Transfer
account={ walletAccount }
balance={ balance }
images={ images }
onClose={ this.onTransferClose }
/>
);
@ -365,7 +363,6 @@ function mapStateToProps (_, initProps) {
const { isTest } = state.nodeStatus;
const { accountsInfo = {}, accounts = {} } = state.personal;
const { balances } = state.balances;
const { images } = state;
const walletAccount = accounts[address] || accountsInfo[address] || null;
if (walletAccount) {
@ -379,7 +376,6 @@ function mapStateToProps (_, initProps) {
return {
address,
balance,
images,
isTest,
owned,
wallet,

View File

@ -28,7 +28,8 @@ import ListIcon from 'material-ui/svg-icons/action/view-list';
import SettingsIcon from 'material-ui/svg-icons/action/settings';
import SendIcon from 'material-ui/svg-icons/content/send';
import { Actionbar, ActionbarExport, ActionbarImport, Button, Editor, Page, Select, Input } from '~/ui';
import { Actionbar, ActionbarExport, ActionbarImport, Button, Page, Select, Input } from '~/ui';
import Editor from '~/ui/Editor';
import { DeployContract, SaveContract, LoadContract } from '~/modals';
import WriteContractStore from './writeContractStore';

View File

@ -14,44 +14,20 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import Account from './Account';
import Accounts from './Accounts';
import Address from './Address';
import Addresses from './Addresses';
import Application from './Application';
import Contract from './Contract';
import Contracts from './Contracts';
import Dapp from './Dapp';
import Dapps from './Dapps';
import HistoryStore from './historyStore';
import ParityBar from './ParityBar';
import Settings, { SettingsBackground, SettingsParity, SettingsProxy, SettingsViews } from './Settings';
import Signer from './Signer';
import Status from './Status';
import Wallet from './Wallet';
import Web from './Web';
import WriteContract from './WriteContract';
export {
Account,
Accounts,
Address,
Addresses,
Application,
Contract,
Contracts,
Dapp,
Dapps,
HistoryStore,
ParityBar,
Settings,
SettingsBackground,
SettingsParity,
SettingsProxy,
SettingsViews,
Signer,
Status,
Wallet,
Web,
WriteContract
};
export Account from './Account';
export Accounts from './Accounts';
export Address from './Address';
export Addresses from './Addresses';
export Application from './Application';
export Contract from './Contract';
export Contracts from './Contracts';
export Dapp from './Dapp';
export Dapps from './Dapps';
export HistoryStore from './historyStore';
export ParityBar from './ParityBar';
export Settings, { SettingsBackground, SettingsParity, SettingsProxy, SettingsViews } from './Settings';
export Signer from './Signer';
export Status from './Status';
export Wallet from './Wallet';
export Web from './Web';
export WriteContract from './WriteContract';

View File

@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
try {
var JsonRpc = require('../.npmjs/jsonRpc/library.js');
var JsonRpc = require('../.npmjs/jsonrpc/library.js').default;
if (typeof JsonRpc !== 'object') {
throw new Error('JsonRpc');

View File

@ -32,17 +32,25 @@ const FAVICON = path.resolve(__dirname, '../assets/images/parity-logo-black-no-t
const DEST = process.env.BUILD_DEST || '.build';
const ENV = process.env.NODE_ENV || 'development';
const EMBED = process.env.EMBED;
const isProd = ENV === 'production';
const isEmbed = EMBED === '1' || EMBED === 'true';
const entry = isEmbed
? {
embed: './embed.js'
}
: Object.assign({}, Shared.dappsEntry, {
index: './index.js'
});
module.exports = {
cache: !isProd,
devtool: isProd ? '#hidden-source-map' : '#source-map',
context: path.join(__dirname, '../src'),
entry: Object.assign({}, Shared.dappsEntry, {
index: './index.js',
embed: './embed.js'
}),
entry: entry,
output: {
// publicPath: '/',
path: path.join(__dirname, '../', DEST),
@ -55,15 +63,12 @@ module.exports = {
test: /\.js$/,
exclude: /(node_modules)/,
// use: [ 'happypack/loader?id=js' ]
use: isProd ? ['babel-loader'] : [
'babel-loader?cacheDirectory=true'
],
options: Shared.getBabelrc()
use: isProd ? 'babel-loader' : 'babel-loader?cacheDirectory=true'
},
{
test: /\.js$/,
include: /node_modules\/material-ui-chip-input/,
use: [ 'babel-loader' ]
include: /node_modules\/(material-chip-input|ethereumjs-tx)/,
use: 'babel-loader'
},
{
test: /\.json$/,
@ -91,13 +96,13 @@ module.exports = {
test: /\.css$/,
include: [ /src/ ],
// exclude: [ /src\/dapps/ ],
loader: isProd ? ExtractTextPlugin.extract([
loader: (isProd && !isEmbed) ? ExtractTextPlugin.extract([
// 'style-loader',
'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]',
'postcss-loader'
]) : undefined,
// use: [ 'happypack/loader?id=css' ]
use: isProd ? undefined : [
use: (isProd && !isEmbed) ? undefined : [
'style-loader',
'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]',
'postcss-loader'
@ -155,18 +160,13 @@ module.exports = {
});
});
const plugins = Shared.getPlugins().concat(
new CopyWebpackPlugin([
{ from: './error_pages.css', to: 'styles.css' },
{ from: 'dapps/static' }
], {}),
let plugins = Shared.getPlugins().concat(
new WebpackErrorNotificationPlugin()
);
new WebpackErrorNotificationPlugin(),
new webpack.DllReferencePlugin({
context: '.',
manifest: require(`../${DEST}/vendor-manifest.json`)
}),
if (!isEmbed) {
plugins = [].concat(
plugins,
new HtmlWebpackPlugin({
title: 'Parity',
@ -179,6 +179,31 @@ module.exports = {
]
}),
new ServiceWorkerWebpackPlugin({
entry: path.join(__dirname, '../src/serviceWorker.js')
}),
DappsHTMLInjection,
new webpack.DllReferencePlugin({
context: '.',
manifest: require(`../${DEST}/vendor-manifest.json`)
}),
new ScriptExtHtmlWebpackPlugin({
sync: [ 'commons', 'vendor.js' ],
defaultAttribute: 'defer'
}),
new CopyWebpackPlugin([
{ from: './error_pages.css', to: 'styles.css' },
{ from: 'dapps/static' }
], {})
);
}
if (isEmbed) {
plugins.push(
new HtmlWebpackPlugin({
title: 'Parity Bar',
filename: 'embed.html',
@ -188,19 +213,9 @@ module.exports = {
isProd ? null : 'commons',
'embed'
]
}),
new ScriptExtHtmlWebpackPlugin({
sync: [ 'commons', 'vendor.js' ],
defaultAttribute: 'defer'
}),
new ServiceWorkerWebpackPlugin({
entry: path.join(__dirname, '../src/serviceWorker.js')
}),
DappsHTMLInjection
})
);
}
if (!isProd) {
const DEST_I18N = path.join(__dirname, '..', DEST, 'i18n');

View File

@ -36,4 +36,5 @@ app.use(wsProxy);
var server = app.listen(process.env.PORT || 3000, function () {
console.log('Listening on port', server.address().port);
});
server.on('upgrade', wsProxy.upgrade);

49
js/webpack/embed.js Normal file
View File

@ -0,0 +1,49 @@
// Copyright 2015-2017 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/>.
const webpack = require('webpack');
const WebpackStats = require('webpack/lib/Stats');
const path = require('path');
const fs = require('fs');
const WebpackConfig = require('./app');
const compiler = webpack(WebpackConfig);
compiler.run(function handler (err, stats) {
if (err) {
return console.error(err);
}
// @see https://github.com/webpack/webpack/blob/324d309107f00cfc38ec727521563d309339b2ec/lib/Stats.js#L790
// Accepted values: none, errors-only, minimal, normal, verbose
const options = WebpackStats.presetToOptions('normal');
options.timings = true;
const output = stats.toString(options);
const assets = Object.keys(stats.compilation.assets);
const embedPath = path.resolve(WebpackConfig.output.path, './embed.json');
const embedData = { assets: assets };
fs.writeFileSync(embedPath, JSON.stringify(embedData, null, 2));
process.stdout.write('\n');
process.stdout.write(output);
process.stdout.write('\n\n');
});

View File

@ -26,6 +26,7 @@ const rucksack = require('rucksack-css');
const CircularDependencyPlugin = require('circular-dependency-plugin');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const EMBED = process.env.EMBED;
const ENV = process.env.NODE_ENV || 'development';
const isProd = ENV === 'production';
@ -113,6 +114,7 @@ function getPlugins (_isProd = isProd) {
new webpack.DefinePlugin({
'process.env': {
EMBED: JSON.stringify(EMBED),
NODE_ENV: JSON.stringify(ENV),
RPC_ADDRESS: JSON.stringify(process.env.RPC_ADDRESS),
PARITY_URL: JSON.stringify(process.env.PARITY_URL),

View File

@ -37,7 +37,7 @@
0ACF9AC61E30FAB600D5C935 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
0ACF9AC81E30FAB600D5C935 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0AE564F01E3CE42C00BD01F7 /* GetBSDProcessList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetBSDProcessList.swift; sourceTree = "<group>"; };
0AED4D9F1E3E22F800BF87C0 /* ethstore */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = ethstore; path = ../target/release/deps/ethstore; sourceTree = "<group>"; };
0AED4D9F1E3E22F800BF87C0 /* ethstore */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = ethstore; path = ../target/release/ethstore; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */

View File

@ -18,7 +18,7 @@ use std::path::PathBuf;
use ethcore::ethstore::{EthStore, SecretStore, import_accounts, read_geth_accounts};
use ethcore::ethstore::dir::RootDiskDirectory;
use ethcore::ethstore::SecretVaultRef;
use ethcore::account_provider::AccountProvider;
use ethcore::account_provider::{AccountProvider, AccountProviderSettings};
use helpers::{password_prompt, password_from_file};
use params::SpecType;
@ -92,7 +92,7 @@ fn new(n: NewAccount) -> Result<String, String> {
let dir = Box::new(keys_dir(n.path, n.spec)?);
let secret_store = Box::new(secret_store(dir, Some(n.iterations))?);
let acc_provider = AccountProvider::new(secret_store);
let acc_provider = AccountProvider::new(secret_store, AccountProviderSettings::default());
let new_account = acc_provider.new_account(&password).map_err(|e| format!("Could not create new account: {}", e))?;
Ok(format!("{:?}", new_account))
}
@ -100,7 +100,7 @@ fn new(n: NewAccount) -> Result<String, String> {
fn list(list_cmd: ListAccounts) -> Result<String, String> {
let dir = Box::new(keys_dir(list_cmd.path, list_cmd.spec)?);
let secret_store = Box::new(secret_store(dir, None)?);
let acc_provider = AccountProvider::new(secret_store);
let acc_provider = AccountProvider::new(secret_store, AccountProviderSettings::default());
let accounts = acc_provider.accounts();
let result = accounts.into_iter()
.map(|a| format!("{:?}", a))

View File

@ -101,6 +101,9 @@ usage! {
or |c: &Config| otry!(c.account).password.clone(),
flag_keys_iterations: u32 = 10240u32,
or |c: &Config| otry!(c.account).keys_iterations.clone(),
flag_no_hardware_wallets: bool = false,
or |c: &Config| otry!(c.account).disable_hardware.clone(),
flag_force_ui: bool = false,
or |c: &Config| otry!(c.ui).force.clone(),
@ -347,6 +350,7 @@ struct Account {
unlock: Option<Vec<String>>,
password: Option<Vec<String>>,
keys_iterations: Option<u32>,
disable_hardware: Option<bool>,
}
#[derive(Default, Debug, PartialEq, RustcDecodable)]
@ -583,6 +587,7 @@ mod tests {
flag_unlock: Some("0xdeadbeefcafe0000000000000000000000000000".into()),
flag_password: vec!["~/.safe/password.file".into()],
flag_keys_iterations: 10240u32,
flag_no_hardware_wallets: false,
flag_force_ui: false,
flag_no_ui: false,
@ -769,6 +774,7 @@ mod tests {
unlock: Some(vec!["0x1".into(), "0x2".into(), "0x3".into()]),
password: Some(vec!["passwdfile path".into()]),
keys_iterations: None,
disable_hardware: None,
}),
ui: Some(Ui {
force: None,

View File

@ -78,6 +78,7 @@ Account Options:
--keys-iterations NUM Specify the number of iterations to use when
deriving key from the password (bigger is more
secure) (default: {flag_keys_iterations}).
--no-hardware-wallets Disables hardware wallet support. (default: {flag_no_hardware_wallets})
UI Options:
--force-ui Enable Trusted UI WebSocket endpoint,

View File

@ -461,6 +461,7 @@ impl Configuration {
testnet: self.args.flag_testnet,
password_files: self.args.flag_password.clone(),
unlocked_accounts: to_addresses(&self.args.flag_unlock)?,
enable_hardware_wallets: !self.args.flag_no_hardware_wallets,
};
Ok(cfg)

View File

@ -175,6 +175,7 @@ pub struct AccountsConfig {
pub testnet: bool,
pub password_files: Vec<String>,
pub unlocked_accounts: Vec<Address>,
pub enable_hardware_wallets: bool,
}
impl Default for AccountsConfig {
@ -184,6 +185,7 @@ impl Default for AccountsConfig {
testnet: false,
password_files: Vec::new(),
unlocked_accounts: Vec::new(),
enable_hardware_wallets: true,
}
}
}

View File

@ -16,7 +16,7 @@
use ethcore::ethstore::{PresaleWallet, EthStore};
use ethcore::ethstore::dir::RootDiskDirectory;
use ethcore::account_provider::AccountProvider;
use ethcore::account_provider::{AccountProvider, AccountProviderSettings};
use helpers::{password_prompt, password_from_file};
use params::SpecType;
@ -37,7 +37,7 @@ pub fn execute(cmd: ImportWallet) -> Result<String, String> {
let dir = Box::new(RootDiskDirectory::create(cmd.path).unwrap());
let secret_store = Box::new(EthStore::open_with_iterations(dir, cmd.iterations).unwrap());
let acc_provider = AccountProvider::new(secret_store);
let acc_provider = AccountProvider::new(secret_store, AccountProviderSettings::default());
let wallet = PresaleWallet::open(cmd.wallet_path).map_err(|_| "Unable to open presale wallet.")?;
let kp = wallet.decrypt(&password).map_err(|_| "Invalid password.")?;
let address = acc_provider.insert_account(kp.secret().clone(), &password).unwrap();

View File

@ -26,7 +26,7 @@ use ethcore_logger::{Config as LogConfig};
use ethcore::miner::{StratumOptions, Stratum};
use ethcore::client::{Mode, DatabaseCompactionProfile, VMType, BlockChainClient};
use ethcore::service::ClientService;
use ethcore::account_provider::AccountProvider;
use ethcore::account_provider::{AccountProvider, AccountProviderSettings};
use ethcore::miner::{Miner, MinerService, ExternalMiner, MinerOptions};
use ethcore::snapshot;
use ethcore::verification::queue::VerifierSettings;
@ -516,9 +516,13 @@ fn prepare_account_provider(spec: &SpecType, dirs: &Directories, data_dir: &str,
let path = dirs.keys_path(data_dir);
upgrade_key_location(&dirs.legacy_keys_path(cfg.testnet), &path);
let dir = Box::new(RootDiskDirectory::create(&path).map_err(|e| format!("Could not open keys directory: {}", e))?);
let account_provider = AccountProvider::new(Box::new(
EthStore::open_with_iterations(dir, cfg.iterations).map_err(|e| format!("Could not open keys directory: {}", e))?
));
let account_settings = AccountProviderSettings {
enable_hardware_wallets: cfg.enable_hardware_wallets,
hardware_wallet_classic_key: spec == &SpecType::Classic,
};
let account_provider = AccountProvider::new(
Box::new(EthStore::open_with_iterations(dir, cfg.iterations).map_err(|e| format!("Could not open keys directory: {}", e))?),
account_settings);
for a in cfg.unlocked_accounts {
// Check if the account exists

View File

@ -21,6 +21,7 @@ use std::ops::Deref;
use std::sync::Weak;
use futures::{future, Future, BoxFuture};
use rlp::{self, Stream};
use util::{Address, H520, H256, U256, Uint, Bytes};
use util::sha3::Hashable;
@ -129,12 +130,31 @@ impl<C: MiningBlockChainClient, M: MinerService> Dispatcher for FullDispatcher<C
};
let hash = t.hash(network_id);
if accounts.is_hardware_address(address) {
let mut stream = rlp::RlpStream::new();
t.rlp_append_unsigned_transaction(&mut stream, network_id);
let signature = try_bf!(
accounts.sign_with_hardware(address, &stream.as_raw())
.map_err(|e| {
debug!(target: "miner", "Error signing transaction with hardware wallet: {}", e);
errors::account("Error signing transaction with hardware wallet", e)
})
);
let signed = try_bf!(
SignedTransaction::new(t.with_signature(signature, network_id))
.map_err(|e| {
debug!(target: "miner", "Hardware wallet has produced invalid signature: {}", e);
errors::account("Invalid signature generated", e)
})
);
WithToken::No(signed)
} else {
let signature = try_bf!(signature(accounts, address, hash, password));
signature.map(|sig| {
SignedTransaction::new(t.with_signature(sig, network_id))
.expect("Transaction was signed by AccountsProvider; it never produces invalid signatures; qed")
})
}
}).boxed()
}
@ -309,7 +329,7 @@ fn decrypt(accounts: &AccountProvider, address: Address, msg: Bytes, password: S
})
}
/// Extract default gas price from a client and miner.
/// Extract the default gas price from a client and miner.
pub fn default_gas_price<C, M>(client: &C, miner: &M) -> U256
where C: MiningBlockChainClient, M: MinerService
{

View File

@ -45,6 +45,7 @@ use v1::types::{
TransactionStats, LocalTransactionStatus,
BlockNumber, ConsensusCapability, VersionInfo,
OperationsInfo, DappId, ChainStatus,
AccountInfo, HwAccountInfo
};
/// Parity implementation.
@ -111,7 +112,7 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
{
type Metadata = Metadata;
fn accounts_info(&self, dapp: Trailing<DappId>) -> Result<BTreeMap<String, BTreeMap<String, String>>, Error> {
fn accounts_info(&self, dapp: Trailing<DappId>) -> Result<BTreeMap<H160, AccountInfo>, Error> {
let dapp = dapp.0;
let store = take_weak!(self.accounts);
@ -128,12 +129,17 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
.into_iter()
.chain(other.into_iter())
.filter(|&(ref a, _)| dapp_accounts.contains(a))
.map(|(a, v)| {
let m = map![
"name".to_owned() => v.name
];
(format!("0x{}", a.hex()), m)
})
.map(|(a, v)| (H160::from(a), AccountInfo { name: v.name }))
.collect()
)
}
fn hardware_accounts_info(&self) -> Result<BTreeMap<H160, HwAccountInfo>, Error> {
let store = take_weak!(self.accounts);
let info = store.hardware_accounts_info().map_err(|e| errors::account("Could not fetch account info.", e))?;
Ok(info
.into_iter()
.map(|(a, v)| (H160::from(a), HwAccountInfo { name: v.name, manufacturer: v.meta }))
.collect()
)
}

View File

@ -16,7 +16,7 @@
use std::sync::Arc;
use ethcore::account_provider::AccountProvider;
use ethcore::account_provider::{AccountProvider, AccountProviderSettings};
use ethstore::EthStore;
use ethstore::dir::RootDiskDirectory;
use devtools::RandomTempPath;
@ -36,7 +36,7 @@ fn accounts_provider() -> Arc<AccountProvider> {
fn accounts_provider_with_vaults_support(temp_path: &str) -> Arc<AccountProvider> {
let root_keys_dir = RootDiskDirectory::create(temp_path).unwrap();
let secret_store = EthStore::open(Box::new(root_keys_dir)).unwrap();
Arc::new(AccountProvider::new(Box::new(secret_store)))
Arc::new(AccountProvider::new(Box::new(secret_store), AccountProviderSettings::default()))
}
fn setup_with_accounts_provider(accounts_provider: Arc<AccountProvider>) -> ParityAccountsTester {
@ -314,6 +314,14 @@ fn rpc_parity_vault_adds_vault_field_to_acount_meta() {
let response = format!(r#"{{"jsonrpc":"2.0","result":{{"0x{}":{{"meta":"{{\"vault\":\"vault1\"}}","name":"","uuid":"{}"}}}},"id":1}}"#, address1.hex(), uuid1);
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
// and then
assert!(tester.accounts.change_vault(address1, "").is_ok());
let request = r#"{"jsonrpc": "2.0", "method": "parity_allAccountsInfo", "params":[], "id": 1}"#;
let response = format!(r#"{{"jsonrpc":"2.0","result":{{"0x{}":{{"meta":"{{}}","name":"","uuid":"{}"}}}},"id":1}}"#, address1.hex(), uuid1);
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
}
#[test]
@ -358,6 +366,14 @@ fn rpc_parity_get_set_vault_meta() {
let tester = setup_with_vaults_support(temp_path.as_str());
assert!(tester.accounts.create_vault("vault1", "password1").is_ok());
// when no meta set
let request = r#"{"jsonrpc": "2.0", "method": "parity_getVaultMeta", "params":["vault1"], "id": 1}"#;
let response = r#"{"jsonrpc":"2.0","result":"{}","id":1}"#;
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
// when meta set
assert!(tester.accounts.set_vault_meta("vault1", "vault1_meta").is_ok());
let request = r#"{"jsonrpc": "2.0", "method": "parity_getVaultMeta", "params":["vault1"], "id": 1}"#;
@ -365,11 +381,13 @@ fn rpc_parity_get_set_vault_meta() {
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
// change meta
let request = r#"{"jsonrpc": "2.0", "method": "parity_setVaultMeta", "params":["vault1", "updated_vault1_meta"], "id": 1}"#;
let response = r#"{"jsonrpc":"2.0","result":true,"id":1}"#;
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
// query changed meta
let request = r#"{"jsonrpc": "2.0", "method": "parity_getVaultMeta", "params":["vault1"], "id": 1}"#;
let response = r#"{"jsonrpc":"2.0","result":"updated_vault1_meta","id":1}"#;

View File

@ -28,6 +28,7 @@ use v1::types::{
TransactionStats, LocalTransactionStatus,
BlockNumber, ConsensusCapability, VersionInfo,
OperationsInfo, DappId, ChainStatus,
AccountInfo, HwAccountInfo,
};
build_rpc_trait! {
@ -37,7 +38,11 @@ build_rpc_trait! {
/// Returns accounts information.
#[rpc(name = "parity_accountsInfo")]
fn accounts_info(&self, Trailing<DappId>) -> Result<BTreeMap<String, BTreeMap<String, String>>, Error>;
fn accounts_info(&self, Trailing<DappId>) -> Result<BTreeMap<H160, AccountInfo>, Error>;
/// Returns hardware accounts information.
#[rpc(name = "parity_hardwareAccountsInfo")]
fn hardware_accounts_info(&self) -> Result<BTreeMap<H160, HwAccountInfo>, Error>;
/// Returns default account for dapp.
#[rpc(meta, name = "parity_defaultAccount")]

View File

@ -0,0 +1,31 @@
// Copyright 2015-2017 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/>.
/// Account information.
#[derive(Debug, Default, Clone, PartialEq, Serialize)]
pub struct AccountInfo {
/// Account name
pub name: String,
}
/// Hardware wallet information.
#[derive(Debug, Default, Clone, PartialEq, Serialize)]
pub struct HwAccountInfo {
/// Device name.
pub name: String,
/// Device manufacturer.
pub manufacturer: String,
}

View File

@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
mod account_info;
mod bytes;
mod block;
mod block_number;
@ -65,3 +66,4 @@ pub use self::uint::{U128, U256};
pub use self::work::Work;
pub use self::histogram::Histogram;
pub use self::consensus_status::*;
pub use self::account_info::{AccountInfo, HwAccountInfo};

View File

@ -23,7 +23,7 @@ const SNAPPY_OK: c_int = 0;
const SNAPPY_INVALID_INPUT: c_int = 1;
const SNAPPY_BUFFER_TOO_SMALL: c_int = 2;
#[link(name = "snappy")]
#[link(name = "snappy", kind = "static")]
extern {
fn snappy_compress(
input: *const c_char,