diff --git a/Cargo.lock b/Cargo.lock index a8002eb47..5d7617bf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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)" = "" "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)" = "" @@ -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)" = "" +"checksum libusb-sys 0.2.3 (git+https://github.com/ethcore/libusb-sys)" = "" "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" diff --git a/ethcore/Cargo.toml b/ethcore/Cargo.toml index d31e401f6..1f4d7ea96 100644 --- a/ethcore/Cargo.toml +++ b/ethcore/Cargo.toml @@ -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] diff --git a/ethcore/src/account_provider/mod.rs b/ethcore/src/account_provider/mod.rs index 6bb85eb59..975d2cbc9 100755 --- a/ethcore/src/account_provider/mod.rs +++ b/ethcore/src/account_provider/mod.rs @@ -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 for SignError { + fn from(e: HardwareError) -> Self { + SignError::Hardware(e) + } +} + impl From for SignError { fn from(e: SSError) -> Self { SignError::SStore(e) @@ -107,17 +120,47 @@ pub struct AccountProvider { sstore: Box, /// Accounts unlocked with rolling tokens transient_sstore: EthMultiStore, + /// Accounts in hardware wallets. + hardware_store: Option, +} + +/// 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) -> Self { + pub fn new(sstore: Box, 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, 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>) -> Result<(), Error> { @@ -271,21 +321,43 @@ impl AccountProvider { /// Returns each account along with name and meta. pub fn accounts_info(&self) -> Result, Error> { - let r: HashMap = 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, 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 { - let account = self.sstore.account_ref(&address)?; - Ok(AccountMeta { - name: self.sstore.name(&account)?, - meta: self.sstore.meta(&account)?, - uuid: self.sstore.uuid(&account).ok().map(Into::into), // allowed to not have a Uuid - }) + 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)?, + meta: self.sstore.meta(&account)?, + uuid: self.sstore.uuid(&account).ok().map(Into::into), // allowed to not have a Uuid + }) + } } /// Returns each account along with name and meta. @@ -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 { + 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)] diff --git a/ethcore/src/lib.rs b/ethcore/src/lib.rs index a889232df..c23defe89 100644 --- a/ethcore/src/lib.rs +++ b/ethcore/src/lib.rs @@ -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; diff --git a/ethcore/src/types/transaction.rs b/ethcore/src/types/transaction.rs index 3e25605d4..506bf001e 100644 --- a/ethcore/src/types/transaction.rs +++ b/ethcore/src/types/transaction.rs @@ -154,7 +154,7 @@ impl Transaction { pub fn hash(&self, network_id: Option) -> 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`. diff --git a/ethkey/src/extended.rs b/ethkey/src/extended.rs index c5426601f..2d2e27b5b 100644 --- a/ethkey/src/extended.rs +++ b/ethkey/src/extended.rs @@ -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 { + /// Soft key derivation (allow proof of parent) + Soft(T), + /// Hard key derivation (does not allow proof of parent) + Hard(T), +} + +impl From for Derivation { + 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(&self, index: Derivation) -> 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 { + pub fn derive(&self, index: Derivation) -> Result 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 { + pub fn derive(&self, index: Derivation) -> Result 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(private_key: H256, chain_code: H256, index: Derivation) -> (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(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(private_key: H256, chain_code: H256, index: T) -> (H256, H256) where T: Label { + let mut data: Vec = 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(public_key: H512, chain_code: H256, derivation: Derivation) -> 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") ); diff --git a/ethstore/src/bin/ethstore.rs b/ethstore/src/bin/ethstore.rs index 5128e9ffb..06a0b40a8 100644 --- a/ethstore/src/bin/ethstore.rs +++ b/ethstore/src/bin/ethstore.rs @@ -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, 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(command: I) -> Result where I: IntoIterator = 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(command: I) -> Result where I: IntoIterator VaultKeyDirectoryProvider for DiskDirectory where T: KeyFileManager { }) .collect()) } + + fn vault_meta(&self, name: &str) -> Result { + 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(&self, account: SafeAccount, writer: &mut T) -> Result<(), Error> where T: io::Write { + fn write(&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))) } diff --git a/ethstore/src/dir/mod.rs b/ethstore/src/dir/mod.rs index 79890650b..6e4326968 100755 --- a/ethstore/src/dir/mod.rs +++ b/ethstore/src/dir/mod.rs @@ -72,6 +72,8 @@ pub trait VaultKeyDirectoryProvider { fn open(&self, name: &str, key: VaultKey) -> Result, Error>; /// List all vaults fn list_vaults(&self) -> Result, Error>; + /// Get vault meta + fn vault_meta(&self, name: &str) -> Result; } /// Vault directory diff --git a/ethstore/src/dir/vault.rs b/ethstore/src/dir/vault.rs index 8699a9e49..2e777360a 100755 --- a/ethstore/src/dir/vault.rs +++ b/ethstore/src/dir/vault.rs @@ -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

(root: P, name: &str) -> Result where P: AsRef { + // 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 { 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

(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

(vault_dir_path: P, key: &VaultKey) -> Result where P: AsRef { +fn read_vault_file

(vault_dir_path: P, key: Option<&VaultKey>) -> Result where P: AsRef { let mut vault_file_path: PathBuf = vault_dir_path.as_ref().into(); vault_file_path.push(VAULT_FILE_NAME); @@ -250,10 +262,12 @@ fn read_vault_file

(vault_dir_path: P, key: &VaultKey) -> Result Result { + // 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()); + } } diff --git a/ethstore/src/json/vault_file.rs b/ethstore/src/json/vault_file.rs index eb7440a85..c2d86cd0c 100755 --- a/ethstore/src/json/vault_file.rs +++ b/ethstore/src/json/vault_file.rs @@ -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); + } } diff --git a/hw/Cargo.toml b/hw/Cargo.toml new file mode 100644 index 000000000..39baa675d --- /dev/null +++ b/hw/Cargo.toml @@ -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 "] + +[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" diff --git a/hw/src/ledger.rs b/hw/src/ledger.rs new file mode 100644 index 000000000..50a9ee5f3 --- /dev/null +++ b/hw/src/ledger.rs @@ -0,0 +1,364 @@ +// 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 . + +//! Ledger hardware wallet module. Supports Ledger Blue and Nano S. +/// See https://github.com/LedgerHQ/blue-app-eth/blob/master/doc/ethapp.asc for protocol details. + +use hidapi; +use std::fmt; +use std::cmp::min; +use std::str::FromStr; +use std::time::Duration; +use super::WalletInfo; +use ethkey::{Address, Signature}; +use ethcore_bigint::hash::{H256, FixedHash}; + +const LEDGER_VID: u16 = 0x2c97; +const LEDGER_PIDS: [u16; 2] = [0x0000, 0x0001]; // Nano S and Blue +const ETH_DERIVATION_PATH_BE: [u8; 17] = [ 4, 0x80, 0, 0, 44, 0x80, 0, 0, 60, 0x80, 0, 0, 0, 0, 0, 0, 0 ]; // 44'/60'/0'/0 +const ETC_DERIVATION_PATH_BE: [u8; 21] = [ 5, 0x80, 0, 0, 44, 0x80, 0, 0, 60, 0x80, 0x02, 0x73, 0xd0, 0x80, 0, 0, 0, 0, 0, 0, 0 ]; // 44'/60'/160720'/0'/0 + +const APDU_TAG: u8 = 0x05; +const APDU_CLA: u8 = 0xe0; + +#[cfg(windows)] const HID_PREFIX_ZERO: usize = 1; +#[cfg(not(windows))] const HID_PREFIX_ZERO: usize = 0; + +mod commands { + pub const GET_APP_CONFIGURATION: u8 = 0x06; + pub const GET_ETH_PUBLIC_ADDRESS: u8 = 0x02; + pub const SIGN_ETH_TRANSACTION: u8 = 0x04; +} + +/// Key derivation paths used on ledger wallets. +#[derive(Debug, Clone, Copy)] +pub enum KeyPath { + /// Ethereum. + Ethereum, + /// Ethereum classic. + EthereumClassic, +} + +/// Hardware waller error. +#[derive(Debug)] +pub enum Error { + /// Ethereum wallet protocol error. + Protocol(&'static str), + /// Hidapi error. + Usb(hidapi::HidError), + /// Device with request key is not available. + KeyNotFound, + /// Signing has been cancelled by user. + UserCancel, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + Error::Protocol(ref s) => write!(f, "Ledger protocol error: {}", s), + Error::Usb(ref e) => write!(f, "USB communication error: {}", e), + Error::KeyNotFound => write!(f, "Key not found"), + Error::UserCancel => write!(f, "Operation has been cancelled"), + } + } +} + +impl From for Error { + fn from(err: hidapi::HidError) -> Error { + Error::Usb(err) + } +} + +/// Ledger device manager. +pub struct Manager { + usb: hidapi::HidApi, + devices: Vec, + key_path: KeyPath, +} + +#[derive(Debug)] +struct Device { + path: String, + info: WalletInfo, +} + +impl Manager { + /// Create a new instance. + pub fn new() -> Result { + let manager = Manager { + usb: hidapi::HidApi::new()?, + devices: Vec::new(), + key_path: KeyPath::Ethereum, + }; + Ok(manager) + } + + /// Re-populate device list. Only those devices that have Ethereum app open will be added. + pub fn update_devices(&mut self) -> Result { + self.usb.refresh_devices(); + let devices = self.usb.devices(); + let mut new_devices = Vec::new(); + let mut num_new_devices = 0; + for device in devices { + trace!("Checking device: {:?}", device); + if device.vendor_id != LEDGER_VID || !LEDGER_PIDS.contains(&device.product_id) { + continue; + } + match self.read_device_info(&device) { + Ok(info) => { + debug!("Found device: {:?}", info); + if !self.devices.iter().any(|d| d.path == info.path) { + num_new_devices += 1; + } + new_devices.push(info); + + }, + Err(e) => debug!("Error reading device info: {}", e), + }; + } + self.devices = new_devices; + Ok(num_new_devices) + } + + /// Select key derivation path for a known chain. + pub fn set_key_path(&mut self, key_path: KeyPath) { + self.key_path = key_path; + } + + fn read_device_info(&self, dev_info: &hidapi::HidDeviceInfo) -> Result { + let mut handle = self.open_path(&dev_info.path)?; + let address = Self::read_wallet_address(&mut handle, self.key_path)?; + let manufacturer = dev_info.manufacturer_string.clone().unwrap_or("Unknown".to_owned()); + let name = dev_info.product_string.clone().unwrap_or("Unknown".to_owned()); + let serial = dev_info.serial_number.clone().unwrap_or("Unknown".to_owned()); + Ok(Device { + path: dev_info.path.clone(), + info: WalletInfo { + name: name, + manufacturer: manufacturer, + serial: serial, + address: address, + }, + }) + } + + fn read_wallet_address(handle: &hidapi::HidDevice, key_path: KeyPath) -> Result { + let ver = Self::send_apdu(handle, commands::GET_APP_CONFIGURATION, 0, 0, &[])?; + if ver.len() != 4 { + return Err(Error::Protocol("Version packet size mismatch")); + } + + let (major, minor, patch) = (ver[1], ver[2], ver[3]); + if major < 1 || (major == 1 && minor == 0 && patch < 3) { + return Err(Error::Protocol("App version 1.0.3 is required.")); + } + + let eth_path = Ð_DERIVATION_PATH_BE[..]; + let etc_path = &ETC_DERIVATION_PATH_BE[..]; + let derivation_path = match key_path { + KeyPath::Ethereum => eth_path, + KeyPath::EthereumClassic => etc_path, + }; + let key_and_address = Self::send_apdu(handle, commands::GET_ETH_PUBLIC_ADDRESS, 0, 0, derivation_path)?; + if key_and_address.len() != 107 { // 1 + 65 PK + 1 + 40 Addr (ascii-hex) + return Err(Error::Protocol("Key packet size mismatch")); + } + let address_string = ::std::str::from_utf8(&key_and_address[67..107]) + .map_err(|_| Error::Protocol("Invalid address string"))?; + + let address = Address::from_str(&address_string) + .map_err(|_| Error::Protocol("Invalid address string"))?; + + Ok(address) + } + + /// List connected wallets. This only returns wallets that are ready to be used. + pub fn list_devices(&self) -> Vec { + self.devices.iter().map(|d| d.info.clone()).collect() + } + + /// Get wallet info. + pub fn device_info(&self, address: &Address) -> Option { + self.devices.iter().find(|d| &d.info.address == address).map(|d| d.info.clone()) + } + + /// Sign transaction data with wallet managing `address`. + pub fn sign_transaction(&self, address: &Address, data: &[u8]) -> Result { + let device = self.devices.iter().find(|d| &d.info.address == address) + .ok_or(Error::KeyNotFound)?; + + let handle = self.open_path(&device.path)?; + + let eth_path = Ð_DERIVATION_PATH_BE[..]; + let etc_path = &ETC_DERIVATION_PATH_BE[..]; + let derivation_path = match self.key_path { + KeyPath::Ethereum => eth_path, + KeyPath::EthereumClassic => etc_path, + }; + const MAX_CHUNK_SIZE: usize = 255; + let mut chunk: [u8; MAX_CHUNK_SIZE] = [0; MAX_CHUNK_SIZE]; + &mut chunk[0..derivation_path.len()].copy_from_slice(derivation_path); + let mut dest_offset = derivation_path.len(); + let mut data_pos = 0; + let mut result; + loop { + let p1 = if data_pos == 0 { 0x00 } else { 0x80 }; + let dest_left = MAX_CHUNK_SIZE - dest_offset; + let chunk_data_size = min(dest_left, data.len() - data_pos); + &mut chunk [dest_offset..][0..chunk_data_size].copy_from_slice(&data[data_pos..][0..chunk_data_size]); + result = Self::send_apdu(&handle, commands::SIGN_ETH_TRANSACTION, p1, 0, &chunk[0..(dest_offset + chunk_data_size)])?; + dest_offset = 0; + data_pos += chunk_data_size; + if data_pos == data.len() { + break; + } + } + + if result.len() != 65 { + return Err(Error::Protocol("Signature packet size mismatch")); + } + let v = result[0]; + let r = H256::from_slice(&result[1..33]); + let s = H256::from_slice(&result[33..65]); + Ok(Signature::from_rsv(&r, &s, v)) + } + + fn open_path(&self, path: &str) -> Result { + let mut err = Error::KeyNotFound; + /// Try to open device a few times. + for _ in 0..10 { + match self.usb.open_path(&path) { + Ok(handle) => return Ok(handle), + Err(e) => err = From::from(e), + } + ::std::thread::sleep(Duration::from_millis(200)); + } + Err(err) + } + + fn send_apdu(handle: &hidapi::HidDevice, command: u8, p1: u8, p2: u8, data: &[u8]) -> Result, Error> { + const HID_PACKET_SIZE: usize = 64 + HID_PREFIX_ZERO; + let mut offset = 0; + let mut chunk_index = 0; + loop { + let mut hid_chunk: [u8; HID_PACKET_SIZE] = [0; HID_PACKET_SIZE]; + let mut chunk_size = if chunk_index == 0 { 12 } else { 5 }; + let size = min(64 - chunk_size, data.len() - offset); + { + let mut chunk = &mut hid_chunk[HID_PREFIX_ZERO..]; + &mut chunk[0..5].copy_from_slice(&[0x01, 0x01, APDU_TAG, (chunk_index >> 8) as u8, (chunk_index & 0xff) as u8 ]); + + if chunk_index == 0 { + let data_len = data.len() + 5; + &mut chunk[5..12].copy_from_slice(&[ (data_len >> 8) as u8, (data_len & 0xff) as u8, APDU_CLA, command, p1, p2, data.len() as u8 ]); + } + + &mut chunk[chunk_size..chunk_size + size].copy_from_slice(&data[offset..offset + size]); + offset += size; + chunk_size += size; + } + trace!("writing {:?}", &hid_chunk[..]); + let n = handle.write(&hid_chunk[0..chunk_size])?; + if n < chunk_size { + return Err(Error::Protocol("Write data size mismatch")); + } + if offset == data.len() { + break; + } + chunk_index += 1; + } + + // read response + chunk_index = 0; + let mut message_size = 0; + let mut message = Vec::new(); + loop { + let mut chunk: [u8; HID_PACKET_SIZE] = [0; HID_PACKET_SIZE]; + let chunk_size = handle.read(&mut chunk)?; + trace!("read {:?}", &chunk[..]); + if chunk_size < 5 || chunk[1] != 0x01 || chunk[1] != 0x01 || chunk[2] != APDU_TAG { + return Err(Error::Protocol("Unexpected chunk header")); + } + let seq = (chunk[3] as usize) << 8 | (chunk[4] as usize); + if seq != chunk_index { + return Err(Error::Protocol("Unexpected chunk header")); + } + + let mut offset = 5; + if seq == 0 { + // read message size and status word. + if chunk_size < 7 { + return Err(Error::Protocol("Unexpected chunk header")); + } + message_size = (chunk[5] as usize) << 8 | (chunk[6] as usize); + offset += 2; + } + message.extend_from_slice(&chunk[offset..chunk_size]); + message.truncate(message_size); + if message.len() == message_size { + break; + } + chunk_index +=1; + } + if message.len() < 2 { + return Err(Error::Protocol("No status word")); + } + let status = (message[message.len() - 2] as usize) << 8 | (message[message.len() - 1] as usize); + debug!("Read status {:x}", status); + match status { + 0x6700 => Err(Error::Protocol("Incorrect length")), + 0x6982 => Err(Error::Protocol("Security status not satisfied (Canceled by user)")), + 0x6a80 => Err(Error::Protocol("Invalid data")), + 0x6a82 => Err(Error::Protocol("File not found")), + 0x6a85 => Err(Error::UserCancel), + 0x6b00 => Err(Error::Protocol("Incorrect parameters")), + 0x6d00 => Err(Error::Protocol("Not implemented. Make sure Ethereum app is running.")), + 0x6faa => Err(Error::Protocol("You Ledger need to be unplugged")), + 0x6f00...0x6fff => Err(Error::Protocol("Internal error")), + 0x9000 => Ok(()), + _ => Err(Error::Protocol("Unknown error")), + + }?; + let new_len = message.len() - 2; + message.truncate(new_len); + Ok(message) + } +} + +#[test] +fn smoke() { + use rustc_serialize::hex::FromHex; + let mut manager = Manager::new().unwrap(); + manager.update_devices().unwrap(); + for d in &manager.devices { + println!("Device: {:?}", d); + } + + if let Some(address) = manager.list_devices().first().map(|d| d.address.clone()) { + let tx = FromHex::from_hex("eb018504a817c80082520894a6ca2e6707f2cc189794a9dd459d5b05ed1bcd1c8703f26fcfb7a22480018080").unwrap(); + let signature = manager.sign_transaction(&address, &tx); + println!("Got {:?}", signature); + assert!(signature.is_ok()); + let large_tx = FromHex::from_hex("f8cb81968504e3b2920083024f279475b02a3c39710d6a3f2870d0d788299d48e790f180b8a4b61d27f6000000000000000000000000e1af840a5a1cb1efdf608a97aa632f4aa39ed199000000000000000000000000000000000000000000000000105ff43f46a9a800000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018080").unwrap(); + let signature = manager.sign_transaction(&address, &large_tx); + println!("Got {:?}", signature); + assert!(signature.is_ok()); + let huge_tx = FromHex::from_hex("f935e98201048505d21dba00833b82608080b935946103e86003908155620d2f00600455601460055560a060405260608190527f2e2e2e00000000000000000000000000000000000000000000000000000000006080908152600d805460008290527f2e2e2e00000000000000000000000000000000000000000000000000000000068255909260008051602062003474833981519152602060026001851615610100026000190190941693909304601f0192909204820192909190620000dc565b82800160010185558215620000dc579182015b82811115620000dc578251825591602001919060010190620000bf565b5b50620001009291505b80821115620000fc5760008155600101620000e6565b5090565b5050600e8054600360ff199182168117909255604080518082019091528281527f2e2e2e00000000000000000000000000000000000000000000000000000000006020918201908152600f80546000829052825160069516949094178155937f8d1108e10bcb7c27dddfc02ed9d693a074039d026cf4ea4240b40f7d581ac80260026101006001871615026000190190951694909404601f019290920483019290620001d7565b82800160010185558215620001d7579182015b82811115620001d7578251825591602001919060010190620001ba565b5b50620001fb9291505b80821115620000fc5760008155600101620000e6565b5090565b50506010805460ff19166001179055346200000057604051620034943803806200349483398101604090815281516020830151918301516060840151919390810191015b5b5b60068054600160a060020a0319166c01000000000000000000000000338102041790558151600d80546000829052909160008051602062003474833981519152602060026101006001861615026000190190941693909304601f908101849004820193870190839010620002c157805160ff1916838001178555620002f1565b82800160010185558215620002f1579182015b82811115620002f1578251825591602001919060010190620002d4565b5b50620003159291505b80821115620000fc5760008155600101620000e6565b5090565b505080600f9080519060200190828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106200036557805160ff191683800117855562000395565b8280016001018555821562000395579182015b828111156200039557825182559160200191906001019062000378565b5b50620003b99291505b80821115620000fc5760008155600101620000e6565b5090565b5050600980546c01000000000000000000000000808602819004600160a060020a0319928316179092556007805487840293909304929091169190911790555b505050505b613066806200040e6000396000f300606060405236156102035760e060020a600035046306fdde03811461034b578063095ea7b3146103c65780630b0b6d5b146103ed5780631b1ccc47146103fc57806320e870931461047757806323b872dd1461049657806325b29d84146104c057806327187991146104df578063277ccde2146104f15780632e1fbfcd14610510578063308b2fdc14610532578063313ce5671461055457806338cc48311461057757806340eddc4e146105a057806341f4793a146105bf578063467ed261146105de578063471ad963146105fd5780634e860ebb1461060f5780634efbe9331461061e57806354786b4e1461064257806354e35ba2146106bd57806358793ad4146106d25780635abedab21461073f5780635af2f8211461074e57806360483a3f1461076d57806360d12fa0146107da578063698f2e84146108035780636a749986146108155780636d5f66391461082a5780636e9c36831461083c57806370a082311461085e5780637a290fe5146108805780637e7541461461088f57806394c41bdb1461090a57806395d89b4114610929578063962a64cd146109a4578063a0b6533214610a09578063a9059cbb14610a2b578063ab62438f14610a52578063b63ca98114610aa9578063b7c54c6f14610abb578063c4e41b2214610ada578063ca7c4dba14610af9578063cb79e31b14610b18578063dd62ed3e14610b3a575b6103495b60006000600c546000141561021b57610000565b600354600c54670de0b6b3a764000091349091020204915060009050816001600030600160a060020a031681526020019081526020016000205410156102c557600160a060020a033016600090815260016020526040902054600c54909250828115610000570466038d7ea4c68000023403905033600160a060020a03166108fc829081150290604051809050600060405180830381858888f1935050505015156102c557610000565b5b600160a060020a03338116600081815260016020908152604080832080548801905530909416825283822080548790039055601380543487900301908190559154600c548551908152918201879052845190949293927f5a0391f2a67f11ed0034b68f8cf14e7e41d6f86e0a7622f2af5ea8f07b488396928290030190a45b5050565b005b3461000057610358610b5f565b60405180806020018281038252838181518152602001915080519060200190808383829060006004602084601f0104600302600f01f150905090810190601f1680156103b85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34610000576103d9600435602435610bed565b604080519115158252519081900360200190f35b3461000057610349610c58565b005b3461000057610358610dbc565b60405180806020018281038252838181518152602001915080519060200190808383829060006004602084601f0104600302600f01f150905090810190601f1680156103b85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b3461000057610484610e5a565b60408051918252519081900360200190f35b34610000576103d9600435602435604435610ef9565b604080519115158252519081900360200190f35b3461000057610484610ff3565b60408051918252519081900360200190f35b3461000057610349600435611002565b005b346100005761048461105a565b60408051918252519081900360200190f35b3461000057610484600435611061565b60408051918252519081900360200190f35b346100005761048460043561108d565b60408051918252519081900360200190f35b34610000576105616110b9565b6040805160ff9092168252519081900360200190f35b34610000576105846110c2565b60408051600160a060020a039092168252519081900360200190f35b34610000576104846110c7565b60408051918252519081900360200190f35b34610000576104846110ce565b60408051918252519081900360200190f35b34610000576104846110d5565b60408051918252519081900360200190f35b3461000057610349600435611174565b005b34610000576103496113b5565b005b34610000576103d9600435611407565b604080519115158252519081900360200190f35b3461000057610358611549565b60405180806020018281038252838181518152602001915080519060200190808383829060006004602084601f0104600302600f01f150905090810190601f1680156103b85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34610000576103496004356024356115e7565b005b346100005760408051602060046024803582810135601f8101859004850286018501909652858552610726958335959394604494939290920191819084018382808284375094965061167e95505050505050565b6040805192835290151560208301528051918290030190f35b3461000057610349611c13565b005b3461000057610484611d46565b60408051918252519081900360200190f35b346100005760408051602060046024803582810135601f81018590048502860185019096528585526107269583359593946044949392909201918190840183828082843750949650611d4d95505050505050565b6040805192835290151560208301528051918290030190f35b3461000057610584612303565b60408051600160a060020a039092168252519081900360200190f35b3461000057610349600435612313565b005b3461000057610349600435602435612347565b005b346100005761034960043561252f565b005b3461000057610484600435612941565b60408051918252519081900360200190f35b346100005761048460043561298d565b60408051918252519081900360200190f35b34610000576103496129ac565b005b3461000057610358612a13565b60405180806020018281038252838181518152602001915080519060200190808383829060006004602084601f0104600302600f01f150905090810190601f1680156103b85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b3461000057610484612ab1565b60408051918252519081900360200190f35b3461000057610358612ab8565b60405180806020018281038252838181518152602001915080519060200190808383829060006004602084601f0104600302600f01f150905090810190601f1680156103b85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b3461000057610484600480803590602001908201803590602001908080601f01602080910402602001604051908101604052809392919081815260200183838082843750949650612b4695505050505050565b60408051918252519081900360200190f35b3461000057610484600435612b63565b60408051918252519081900360200190f35b34610000576103d9600435602435612b8c565b604080519115158252519081900360200190f35b3461000057610349600480803590602001908201803590602001908080601f016020809104026020016040519081016040528093929190818152602001838380828437509496505093359350612c3c92505050565b005b3461000057610349600435612f38565b005b3461000057610484612f90565b60408051918252519081900360200190f35b346100005761048461300c565b60408051918252519081900360200190f35b3461000057610484613013565b60408051918252519081900360200190f35b346100005761048460043561301a565b60408051918252519081900360200190f35b3461000057610484600435602435613039565b60408051918252519081900360200190f35b600d805460408051602060026001851615610100026000190190941693909304601f81018490048402820184019092528181529291830182828015610be55780601f10610bba57610100808354040283529160200191610be5565b820191906000526020600020905b815481529060010190602001808311610bc857829003601f168201915b505050505081565b600160a060020a03338116600081815260026020908152604080832094871680845294825280832086905580518681529051929493927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a35060015b92915050565b601a54600090600160a060020a03161515610c7257610000565b600160a060020a0333166000908152600a60205260409020541515610c9657610000565b600160a060020a0333166000908152601d602052604090205460ff1615610cbc57610000565b601b54426212750090910111610cd157610000565b600160a060020a0333166000818152601d60209081526040808320805460ff19166001179055600a8252918290208054601c805490910190555482519384529083015280517f475c7605c08471fdc551a58d2c318b163628c5852f69323a1b91c34eb0bb09339281900390910190a150601154601c54606490910490604682029010610db857601a5460068054600160a060020a031916606060020a600160a060020a0393841681020417908190556040805191909216815290517f6b8184e23a898262087be50aa3ea5de648451e63f94413e810586c25282d58c2916020908290030190a15b5b50565b604080516020808201835260008252600d8054845160026001831615610100026000190190921691909104601f810184900484028201840190955284815292939091830182828015610e4f5780601f10610e2457610100808354040283529160200191610e4f565b820191906000526020600020905b815481529060010190602001808311610e3257829003601f168201915b505050505090505b90565b600f805460408051602060026001851615610100026000190190941693909304601f8101849004840282018401909252818152600093610ef39391929091830182828015610ee95780601f10610ebe57610100808354040283529160200191610ee9565b820191906000526020600020905b815481529060010190602001808311610ecc57829003601f168201915b5050505050612b46565b90505b90565b600160a060020a038316600090815260016020526040812054829010801590610f495750600160a060020a0380851660009081526002602090815260408083203390941683529290522054829010155b8015610f555750600082115b15610fe757600160a060020a03808516600081815260016020908152604080832080548890039055878516808452818420805489019055848452600283528184203390961684529482529182902080548790039055815186815291517fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a3506001610feb56610feb565b5060005b5b9392505050565b600160a060020a033016315b90565b60065433600160a060020a0390811691161461101d57610000565b600c8190556040805182815290517f0bbd501ef336990995d82b5e3fd82a15abe1ff10c982757a1698ac5d1c3e79579181900360200190a15b5b50565b600b545b90565b6000601882815481101561000057906000526020600020906007020160005b506004015490505b919050565b6000601882815481101561000057906000526020600020906007020160005b506001015490505b919050565b600e5460ff1681565b305b90565b6013545b90565b601c545b90565b600d805460408051602060026001851615610100026000190190941693909304601f8101849004840282018401909252818152600093610ef39391929091830182828015610ee95780601f10610ebe57610100808354040283529160200191610ee9565b820191906000526020600020905b815481529060010190602001808311610ecc57829003601f168201915b5050505050612b46565b90505b90565b600654600090819033600160a060020a0390811691161461119457610000565b60008381526014602052604090205415156111ae57610000565b60008381526014602052604090206005015433600160a060020a039081169116146111d857610000565b60008381526014602052604090206005015460a060020a900460ff16156111fe57610000565b601154600084815260146020526040902060040154606490910460370292508290111561122a57610000565b60008381526014602052604081206005015460a860020a900460ff16600181116100005714156112eb57600954600084815260146020908152604080832060058101546001909101548251840185905282517fa9059cbb000000000000000000000000000000000000000000000000000000008152600160a060020a0392831660048201526024810191909152915194169363a9059cbb93604480840194938390030190829087803b156100005760325a03f1156100005750611389915050565b60008381526014602052604080822060058101546001909101549151600160a060020a039091169282156108fc02929190818181858888f160008881526014602090815260409182902060058101546001909101548351600160a060020a0390921682529181019190915281519297507f2648a7e2f9c34700b91370233666e5118fa8be3e0c21fed4f7402b941df8efdd9650829003019350915050a15b6000838152601460205260409020600501805460a060020a60ff02191660a060020a1790555b5b505050565b60065433600160a060020a039081169116146113d057610000565b6010805460ff191690556040517fb48c7f694f0a3b9b22d7e61c60ff8aebbb107314b6b698fc489ff3f017cb57e090600090a15b5b565b600060006000600760009054906101000a9004600160a060020a0316600160a060020a031663d4884b566000604051602001526040518160e060020a028152600401809050602060405180830381600087803b156100005760325a03f115610000575050604051514210905061147c57610000565b60085433600160a060020a0390811691161461149757610000565b5050600b54600160a060020a03328181166000908152600a6020526040902080549386029384019055601180548401905560128054860190556008549092916114e39130911683610ef9565b506114ee8282612b8c565b50600054601154600b5460408051918252602082018590528051600160a060020a038716927fb4d6befef2def3d17bcb13c2b882ec4fa047f33157446d3e0e6094b2a21609ac92908290030190a4600192505b5b5050919050565b604080516020808201835260008252600f8054845160026001831615610100026000190190921691909104601f810184900484028201840190955284815292939091830182828015610e4f5780601f10610e2457610100808354040283529160200191610e4f565b820191906000526020600020905b815481529060010190602001808311610e3257829003601f168201915b505050505090505b90565b60065433600160a060020a0390811691161461160257610000565b60105460ff16151561161357610000565b600160a060020a0330166000908152600160209081526040808320805485019055600c859055825484019283905580518481529051839286927f10cb430288a1696de11938bc5362c6f8c60e58808237bce4436b93a8573e00c3929081900390910190a45b5b5b5050565b6040805161010081018252600080825260208083018290528351908101845281815292820192909252606081018290526080810182905260a0810182905260c0810182905260e08101829052600654829182918291829133600160a060020a039081169116146116ed57610000565b60115460649004935060056016541115801561170c5750836005540288115b1561171657610000565b61171e612f90565b8811156117305761172d612f90565b97505b60003642604051808484808284378201915050828152602001935050505060405180910390209250600454420191506101006040519081016040528084815260200189815260200188815260200183815260200160008152602001338152602001600081526020016000815260200150905080601460008560001916815260200190815260200160002060008201518160000155602082015181600101556040820151816002019080519060200190828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061182057805160ff191683800117855561184d565b8280016001018555821561184d579182015b8281111561184d578251825591602001919060010190611832565b5b5061186e9291505b8082111561186a5760008155600101611856565b5090565b5050606082015160038201556080820151600482015560a08201516005909101805460c084015160e09094015160f860020a90810281900460a860020a0260a860020a60ff02199582029190910460a060020a0260a060020a60ff0219606060020a95860295909504600160a060020a031990931692909217939093161792909216179055601880546001810180835582818380158290116119c9576007028160070283600052602060002091820191016119c991905b8082111561186a5760006000820160009055600182016000905560028201805460018160011615610100020316600290046000825580601f10611968575061199a565b601f01602090049060005260206000209081019061199a91905b8082111561186a5760008155600101611856565b5090565b5b50506000600382018190556004820155600581018054600160b060020a0319169055600701611925565b5090565b5b505050916000526020600020906007020160005b83909190915060008201518160000155602082015181600101556040820151816002019080519060200190828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f10611a4a57805160ff1916838001178555611a77565b82800160010185558215611a77579182015b82811115611a77578251825591602001919060010190611a5c565b5b50611a989291505b8082111561186a5760008155600101611856565b5090565b5050606082015181600301556080820151816004015560a08201518160050160006101000a815481600160a060020a030219169083606060020a90810204021790555060c08201518160050160146101000a81548160ff021916908360f860020a90810204021790555060e08201518160050160156101000a81548160ff021916908360f860020a90810204021790555050505060166000815460010191905081905550426017819055507f1a1eea7d2a0f099c2f19efb4e101fcf220558c9f4fbc6961b33fbe02d3a7be908389848a3360405180866000191681526020018581526020018481526020018060200183600160a060020a031681526020018281038252848181518152602001915080519060200190808383829060006004602084601f0104600302600f01f150905090810190601f168015611bee5780820380516001836020036101000a031916815260200191505b50965050505050505060405180910390a1826001955095505b5b505050509250929050565b60065460009033600160a060020a03908116911614611c3157610000565b600760009054906101000a9004600160a060020a0316600160a060020a031663d4884b566000604051602001526040518160e060020a028152600401809050602060405180830381600087803b156100005760325a03f1156100005750506040515162dd7c00014210159050611ca657610000565b604051600160a060020a0333811691309091163180156108fc02916000818181858888f1600954909550600160a060020a0316935063a9059cbb9250339150611cef9050612f90565b6000604051602001526040518360e060020a0281526004018083600160a060020a0316815260200182815260200192505050602060405180830381600087803b156100005760325a03f115610000575050505b5b50565b6016545b90565b6040805161010081018252600080825260208083018290528351908101845281815292820192909252606081018290526080810182905260a0810182905260c0810182905260e08101829052600654829182918291829133600160a060020a03908116911614611dbc57610000565b60105460ff1615611dcc57610000565b6000611dd73061298d565b1115611de257610000565b6017546212750001421015611df657610000565b6013546064900493508360055402881115611e1057610000565b30600160a060020a031631881115611e305730600160a060020a03163197505b60003642604051808484808284378201915050828152602001935050505060405180910390209250600454420191506101006040519081016040528084815260200189815260200188815260200183815260200160008152602001338152602001600081526020016001815260200150905080601460008560001916815260200190815260200160002060008201518160000155602082015181600101556040820151816002019080519060200190828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f10611f2057805160ff1916838001178555611f4d565b82800160010185558215611f4d579182015b82811115611f4d578251825591602001919060010190611f32565b5b50611f6e9291505b8082111561186a5760008155600101611856565b5090565b5050606082015160038201556080820151600482015560a08201516005909101805460c084015160e09094015160f860020a90810281900460a860020a0260a860020a60ff02199582029190910460a060020a0260a060020a60ff0219606060020a95860295909504600160a060020a031990931692909217939093161792909216179055601880546001810180835582818380158290116120c9576007028160070283600052602060002091820191016120c991905b8082111561186a5760006000820160009055600182016000905560028201805460018160011615610100020316600290046000825580601f10612068575061209a565b601f01602090049060005260206000209081019061209a91905b8082111561186a5760008155600101611856565b5090565b5b50506000600382018190556004820155600581018054600160b060020a0319169055600701612025565b5090565b5b505050916000526020600020906007020160005b83909190915060008201518160000155602082015181600101556040820151816002019080519060200190828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061214a57805160ff1916838001178555612177565b82800160010185558215612177579182015b8281111561217757825182559160200191906001019061215c565b5b506121989291505b8082111561186a5760008155600101611856565b5090565b5050606082015181600301556080820151816004015560a08201518160050160006101000a815481600160a060020a030219169083606060020a90810204021790555060c08201518160050160146101000a81548160ff021916908360f860020a90810204021790555060e08201518160050160156101000a81548160ff021916908360f860020a908102040217905550505050426017819055507f1a1eea7d2a0f099c2f19efb4e101fcf220558c9f4fbc6961b33fbe02d3a7be908389848a3360405180866000191681526020018581526020018481526020018060200183600160a060020a031681526020018281038252848181518152602001915080519060200190808383829060006004602084601f0104600302600f01f150905090810190601f168015611bee5780820380516001836020036101000a031916815260200191505b50965050505050505060405180910390a1826001955095505b5b505050509250929050565b600654600160a060020a03165b90565b600854600160a060020a03161561232957610000565b60088054600160a060020a031916606060020a838102041790555b50565b60065433600160a060020a0390811691161461236257610000565b60105460ff16151561237357610000565b600760009054906101000a9004600160a060020a0316600160a060020a031663d4884b566000604051602001526040518160e060020a028152600401809050602060405180830381600087803b156100005760325a03f11561000057505060405151421090506123e257610000565b600760009054906101000a9004600160a060020a0316600160a060020a031663cdd933326000604051602001526040518160e060020a028152600401809050602060405180830381600087803b156100005760325a03f11561000057505060405151421015905061245257610000565b600854600160a060020a0316151561246957610000565b6000805482018155600160a060020a03308116808352600160209081526040808520805487019055600b8790556002825280852060088054861687529083529481902080548701905593548451868152945193169391927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a3600054601154600b546040805185815290517f10cb430288a1696de11938bc5362c6f8c60e58808237bce4436b93a8573e00c39181900360200190a45b5b5b5b5b5050565b604080516101008082018352600080835260208084018290528451808201865282815284860152606084018290526080840182905260a0840182905260c0840182905260e0840182905285825260148152848220855180850187528154815260018083015482850152600280840180548a51600019948216159099029390930190921604601f8101859004850287018501895280875296979496879692959394938601938301828280156126245780601f106125f957610100808354040283529160200191612624565b820191906000526020600020905b81548152906001019060200180831161260757829003601f168201915b505050918352505060038201546020820152600482015460408201526005820154600160a060020a038116606083015260ff60a060020a820481161515608084015260a09092019160a860020a909104166001811161000057905250600085815260146020526040902054909350151561269d57610000565b60008481526014602052604090206005015460a060020a900460ff16156126c357610000565b60008481526014602052604090206003015442106126e057610000565b6000848152601460209081526040808320600160a060020a033316845260060190915290205460ff161561271357610000565b600160a060020a0333166000818152600a6020908152604080832054888452601483528184206004810180548301905594845260069094019091529020805460ff19166001179055915061276684612941565b6000858152601460205260409020601880549293509091839081101561000057906000526020600020906007020160005b50600082015481600001556001820154816001015560028201816002019080546001816001161561010002031660029004828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f10612801578054855561283d565b8280016001018555821561283d57600052602060002091601f016020900482015b8281111561283d578254825591600101919060010190612822565b5b5061285e9291505b8082111561186a5760008155600101611856565b5090565b5050600382810154908201556004808301549082015560059182018054929091018054600160a060020a031916606060020a600160a060020a0394851681020417808255825460a060020a60ff021990911660f860020a60a060020a9283900460ff908116820282900490930291909117808455935460a860020a60ff021990941660a860020a9485900490921681020490920291909117905560408051868152339092166020830152818101849052517f8f8bbb8c1937f844f6a094cd4c6eeab8ed5e36f83952e6306ffb2c11fffe5bce916060908290030190a15b50505050565b6000805b60185481101561298657601881815481101561000057906000526020600020906007020160005b505483141561297d57809150612986565b5b600101612945565b5b50919050565b600160a060020a0381166000908152600160205260409020545b919050565b60065433600160a060020a039081169116146129c757610000565b600160a060020a03301660009081526001602052604080822080548354038355829055517fe0987873419fe09d3c9a3e0267f4daf163e812d512f867abaf6bf9822f141a8b9190a15b5b565b60408051602080820183526000825260198054845160026001831615610100026000190190921691909104601f810184900484028201840190955284815292939091830182828015610e4f5780601f10610e2457610100808354040283529160200191610e4f565b820191906000526020600020905b815481529060010190602001808311610e3257829003601f168201915b505050505090505b90565b6011545b90565b600f805460408051602060026001851615610100026000190190941693909304601f81018490048402820184019092528181529291830182828015610be55780601f10610bba57610100808354040283529160200191610be5565b820191906000526020600020905b815481529060010190602001808311610bc857829003601f168201915b505050505081565b6000602082511115612b5757610000565b5060208101515b919050565b6000601882815481101561000057906000526020600020906007020160005b505490505b919050565b600160a060020a033316600090815260016020526040812054829010801590612bb55750600082115b15612c2d57600160a060020a03338116600081815260016020908152604080832080548890039055938716808352918490208054870190558351868152935191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a3506001610c5256610c52565b506000610c52565b5b92915050565b600160a060020a0333166000908152600a60205260409020541515612c6057610000565b600760009054906101000a9004600160a060020a0316600160a060020a031663d4884b566000604051602001526040518160e060020a028152600401809050602060405180830381600087803b156100005760325a03f11561000057505060405151626ebe00014210159050612cd557610000565b601b5415801590612cef5750426019600201546212750001115b15612cf957610000565b6040805160808101825283815260208082018490524262127500018284015233600160a060020a03166000908152600a8252928320546060830152815180516019805495819052939484937f944998273e477b495144fb8794c914197f3ccb46be2900f4698fd0ef743c969560026001841615610100026000190190931692909204601f90810182900483019490910190839010612da257805160ff1916838001178555612dcf565b82800160010185558215612dcf579182015b82811115612dcf578251825591602001919060010190612db4565b5b50612df09291505b8082111561186a5760008155600101611856565b5090565b505060208201518160010160006101000a815481600160a060020a030219169083606060020a908102040217905550604082015181600201556060820151816003015590505060016019600401600033600160a060020a0316815260200190815260200160002060006101000a81548160ff021916908360f860020a9081020402179055507f854a9cc4d907d23cd8dcc72af48dc0e6a87e6f76376a309a0ffa3231ce8e13363383426212750001846040518085600160a060020a031681526020018060200184815260200183600160a060020a031681526020018281038252858181518152602001915080519060200190808383829060006004602084601f0104600302600f01f150905090810190601f168015612f235780820380516001836020036101000a031916815260200191505b509550505050505060405180910390a15b5050565b60065433600160a060020a03908116911614612f5357610000565b600b819055600080546011546040519192909184917f17a7f53ef43da32c3936b4ac2b060caff5c4b03cd24b1c8e96a191eb1ec48d1591a45b5b50565b6000600960009054906101000a9004600160a060020a0316600160a060020a03166370a08231306000604051602001526040518260e060020a0281526004018082600160a060020a03168152602001915050602060405180830381600087803b156100005760325a03f115610000575050604051519150505b90565b6000545b90565b600c545b90565b600160a060020a0381166000908152600a60205260409020545b919050565b600160a060020a038083166000908152600260209081526040808320938516835292905220545b9291505056d7b6990105719101dabeb77144f2a3385c8033acd3af97e9423a695e81ad1eb500000000000000000000000069381683bde924cef65f1c97f7c8fb769a20409300000000000000000000000014f37b574242d366558db61f3335289a5035c506000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000018546573742074657374657220746573746573742063616d700000000000000000000000000000000000000000000000000000000000000000000000000000000354455300000000000000000000000000000000000000000000000000000000001ba033230fce515bea9de982fe85e0ec8fe892984bc3070ad633daab20eb370864d5a05deb41870e2117197b84cb71110fc0508fa7457165cb8cb82cb8d4d801e6e3f1").unwrap(); + let signature = manager.sign_transaction(&address, &huge_tx); + println!("Got {:?}", signature); + assert!(signature.is_ok()); + } +} diff --git a/hw/src/lib.rs b/hw/src/lib.rs new file mode 100644 index 000000000..c364fd015 --- /dev/null +++ b/hw/src/lib.rs @@ -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 . + +//! 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 for Error { + fn from(err: ledger::Error) -> Error { + match err { + ledger::Error::KeyNotFound => Error::KeyNotFound, + _ => Error::LedgerDevice(err), + } + } +} + +impl From for Error { + fn from(err: libusb::Error) -> Error { + Error::Usb(err) + } +} + +/// Hardware wallet management interface. +pub struct HardwareWalletManager { + update_thread: Option>, + exiting: Arc, + ledger: Arc>, +} + +struct EventHandler { + ledger: Weak>, +} + +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 { + 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 { + self.ledger.lock().list_devices() + } + + /// Get connected wallet info. + pub fn wallet_info(&self, address: &Address) -> Option { + self.ledger.lock().device_info(address) + } + + /// Sign transaction data with wallet managing `address`. + pub fn sign_transaction(&self, address: &Address, data: &[u8]) -> Result { + 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(); + } + } +} diff --git a/js/.babelrc b/js/.babelrc index a4f2a2006..6fea8d286 100644 --- a/js/.babelrc +++ b/js/.babelrc @@ -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,7 +25,8 @@ }, "test": { "plugins": [ - ["babel-plugin-webpack-alias", { "config": "webpack/test.js" }] + "transform-runtime", + [ "babel-plugin-webpack-alias", { "config": "webpack/test.js" } ] ] } } diff --git a/js/package.json b/js/package.json index 634ffbc71..590808e3e 100644 --- a/js/package.json +++ b/js/package.json @@ -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 ", @@ -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", diff --git a/js/src/api/format/output.js b/js/src/api/format/output.js index dd61c82b1..dc8421516 100644 --- a/js/src/api/format/output.js +++ b/js/src/api/format/output.js @@ -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 || {}; +} diff --git a/js/src/api/format/output.spec.js b/js/src/api/format/output.spec.js index d744a642c..504cc0687 100644 --- a/js/src/api/format/output.spec.js +++ b/js/src/api/format/output.spec.js @@ -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({}); + }); + }); }); diff --git a/js/src/api/rpc/parity/parity.js b/js/src/api/rpc/parity/parity.js index 160e7513a..7001a9c49 100644 --- a/js/src/api/rpc/parity/parity.js +++ b/js/src/api/rpc/parity/parity.js @@ -15,7 +15,7 @@ // along with Parity. If not, see . 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') diff --git a/js/src/api/subscriptions/personal.js b/js/src/api/subscriptions/personal.js index 1574dcacc..4f386b7c8 100644 --- a/js/src/api/subscriptions/personal.js +++ b/js/src/api/subscriptions/personal.js @@ -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; diff --git a/js/src/dapps/localtx/Transaction/transaction.js b/js/src/dapps/localtx/Transaction/transaction.js index 8bd545305..56a697853 100644 --- a/js/src/dapps/localtx/Transaction/transaction.js +++ b/js/src/dapps/localtx/Transaction/transaction.js @@ -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 }; diff --git a/js/src/dapps/tokenreg/Inputs/Text/input-text.js b/js/src/dapps/tokenreg/Inputs/Text/input-text.js index 7bb8bc98b..0862c6443 100644 --- a/js/src/dapps/tokenreg/Inputs/Text/input-text.js +++ b/js/src/dapps/tokenreg/Inputs/Text/input-text.js @@ -105,22 +105,22 @@ 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 validation - .then(validation => { - this.setValidation({ - ...validation, - disabled: false, - loading: false - }); + return Promise.resolve(validation) + .then((validation) => { + clearTimeout(loadingTimeout); - event.target.focus(); + this.setValidation({ + ...validation, + disabled: false, + loading: false }); - } - this.setValidation(validation); + event.target.focus(); + }); } onKeyDown = (event) => { diff --git a/js/src/dapps/tokenreg/Tokens/Token/token.css b/js/src/dapps/tokenreg/Tokens/Token/token.css index cdd6b2b39..990a16fba 100644 --- a/js/src/dapps/tokenreg/Tokens/Token/token.css +++ b/js/src/dapps/tokenreg/Tokens/Token/token.css @@ -49,7 +49,7 @@ } .token-container { - flex: 1; + flex: 1 1 auto; } .full-width .token-container { diff --git a/js/src/i18n/constants.js b/js/src/i18n/constants.js index 0fa59ff4b..c7d47a0fd 100644 --- a/js/src/i18n/constants.js +++ b/js/src/i18n/constants.js @@ -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 { diff --git a/js/src/i18n/languages.js b/js/src/i18n/languages.js index a62f0827a..76bd136ab 100644 --- a/js/src/i18n/languages.js +++ b/js/src/i18n/languages.js @@ -16,5 +16,6 @@ export default { de: 'Deutsch', - en: 'English' + en: 'English', + nl: 'Nederlands' }; diff --git a/js/src/i18n/nl/index.js b/js/src/i18n/nl/index.js new file mode 100644 index 000000000..6b50c2d53 --- /dev/null +++ b/js/src/i18n/nl/index.js @@ -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 . + +import settings from './settings'; + +export default { + settings +}; diff --git a/js/src/i18n/nl/settings.js b/js/src/i18n/nl/settings.js new file mode 100644 index 000000000..9240859ad --- /dev/null +++ b/js/src/i18n/nl/settings.js @@ -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 . + +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' + } + } +}; diff --git a/js/src/jsonrpc/interfaces/parity.js b/js/src/jsonrpc/interfaces/parity.js index 97e52fe73..978c77f88 100644 --- a/js/src/jsonrpc/interfaces/parity.js +++ b/js/src/jsonrpc/interfaces/parity.js @@ -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', diff --git a/js/src/library.etherscan.js b/js/src/library.etherscan.js index 48afee0ea..c4cd9e5f9 100644 --- a/js/src/library.etherscan.js +++ b/js/src/library.etherscan.js @@ -31,4 +31,4 @@ if (isNode) { import Etherscan from './3rdparty/etherscan'; -module.exports = Etherscan; +export default Etherscan; diff --git a/js/src/library.jsonrpc.js b/js/src/library.jsonrpc.js index c409ee421..a288e14be 100644 --- a/js/src/library.jsonrpc.js +++ b/js/src/library.jsonrpc.js @@ -16,4 +16,4 @@ import JsonRpc from './jsonrpc'; -module.exports = JsonRpc; +export default JsonRpc; diff --git a/js/src/library.parity.js b/js/src/library.parity.js index add57c62b..81dc8e885 100644 --- a/js/src/library.parity.js +++ b/js/src/library.parity.js @@ -32,4 +32,4 @@ if (isNode) { import Abi from './abi'; import Api from './api'; -module.exports = { Api, Abi }; +export { Api, Abi }; diff --git a/js/src/library.shapeshift.js b/js/src/library.shapeshift.js index 82bf6235c..ab4b5dedd 100644 --- a/js/src/library.shapeshift.js +++ b/js/src/library.shapeshift.js @@ -31,4 +31,4 @@ if (isNode) { import ShapeShift from './3rdparty/shapeshift'; -module.exports = ShapeShift; +export default ShapeShift; diff --git a/js/src/modals/CreateAccount/NewAccount/newAccount.js b/js/src/modals/CreateAccount/NewAccount/newAccount.js index a8f7ca5db..2eeb10801 100644 --- a/js/src/modals/CreateAccount/NewAccount/newAccount.js +++ b/js/src/modals/CreateAccount/NewAccount/newAccount.js @@ -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'; diff --git a/js/src/modals/CreateAccount/RawKey/rawKey.js b/js/src/modals/CreateAccount/RawKey/rawKey.js index 9c807cac1..87cce86d8 100644 --- a/js/src/modals/CreateAccount/RawKey/rawKey.js +++ b/js/src/modals/CreateAccount/RawKey/rawKey.js @@ -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'; diff --git a/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js b/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js index 3afb21b73..4e27ce4db 100644 --- a/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js +++ b/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js @@ -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'; diff --git a/js/src/modals/LoadContract/loadContract.js b/js/src/modals/LoadContract/loadContract.js index 1bc49a2e3..864572594 100644 --- a/js/src/modals/LoadContract/loadContract.js +++ b/js/src/modals/LoadContract/loadContract.js @@ -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'; diff --git a/js/src/modals/PasswordManager/passwordManager.js b/js/src/modals/PasswordManager/passwordManager.js index f3afae539..d769f6838 100644 --- a/js/src/modals/PasswordManager/passwordManager.js +++ b/js/src/modals/PasswordManager/passwordManager.js @@ -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'; diff --git a/js/src/modals/SaveContract/saveContract.js b/js/src/modals/SaveContract/saveContract.js index af6e07e57..cd9285336 100644 --- a/js/src/modals/SaveContract/saveContract.js +++ b/js/src/modals/SaveContract/saveContract.js @@ -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'; diff --git a/js/src/modals/Transfer/Details/details.js b/js/src/modals/Transfer/Details/details.js index 97d42ffef..a1d0cc637 100644 --- a/js/src/modals/Transfer/Details/details.js +++ b/js/src/modals/Transfer/Details/details.js @@ -14,15 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -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 = ( -

- -
- { token.name } -
-
- { value } { token.tag } -
-
- ); - - return ( - - { label } - - ); - }); - - this.setState({ items }); - } - - render () { - const { tag, onChange } = this.props; - const { items } = this.state; - - return ( - - ); - } -} - 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 ( diff --git a/js/src/modals/Transfer/Details/tokenSelect.js b/js/src/modals/Transfer/Details/tokenSelect.js new file mode 100644 index 000000000..47f42edaa --- /dev/null +++ b/js/src/modals/Transfer/Details/tokenSelect.js @@ -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 . + +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 = ( +
+ +
+ { token.name } +
+
+ { value } { token.tag } +
+
+ ); + + return ( + + { label } + + ); + }); + + this.setState({ items }); + } + + render () { + const { tag, onChange } = this.props; + const { items } = this.state; + + return ( + + ); + } +} diff --git a/js/src/modals/Transfer/transfer.js b/js/src/modals/Transfer/transfer.js index d636b52ae..8751a1cd1 100644 --- a/js/src/modals/Transfer/transfer.js +++ b/js/src/modals/Transfer/transfer.js @@ -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 } diff --git a/js/src/redux/providers/workerWrapper.js b/js/src/redux/providers/workerWrapper.js new file mode 100644 index 000000000..c6bb5daca --- /dev/null +++ b/js/src/redux/providers/workerWrapper.js @@ -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 . + +if (!process.env.EMBED) { + const setupWorker = require('./worker').setupWorker; + + module.exports = { setupWorker }; +} else { + module.exports = { setupWorker: () => {} }; +} diff --git a/js/src/redux/store.js b/js/src/redux/store.js index e49b33369..b4396f517 100644 --- a/js/src/redux/store.js +++ b/js/src/redux/store.js @@ -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, diff --git a/js/src/ui/AccountCard/accountCard.spec.js b/js/src/ui/AccountCard/accountCard.spec.js index ba4791778..7a5b68428 100644 --- a/js/src/ui/AccountCard/accountCard.spec.js +++ b/js/src/ui/AccountCard/accountCard.spec.js @@ -65,7 +65,7 @@ describe('ui/AccountCard', () => { let balance; beforeEach(() => { - balance = component.find('Connect(Balance)'); + balance = component.find('Balance'); }); it('renders the balance', () => { diff --git a/js/src/ui/Actionbar/index.js b/js/src/ui/Actionbar/index.js index dad61a7ba..a057ff730 100644 --- a/js/src/ui/Actionbar/index.js +++ b/js/src/ui/Actionbar/index.js @@ -14,4 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +export Export from './Export'; +export Import from './Import'; +export Search from './Search'; +export Sort from './Sort'; + export default from './actionbar'; diff --git a/js/src/ui/Balance/balance.js b/js/src/ui/Balance/balance.js index c024b1d06..04fed6942 100644 --- a/js/src/ui/Balance/balance.js +++ b/js/src/ui/Balance/balance.js @@ -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 (
- { +
{ value }
@@ -125,14 +109,3 @@ class Balance extends Component { ); } } - -function mapStateToProps (state) { - const { images } = state; - - return { images }; -} - -export default connect( - mapStateToProps, - null -)(Balance); diff --git a/js/src/ui/Balance/balance.spec.js b/js/src/ui/Balance/balance.spec.js index f12e84851..8a886b39b 100644 --- a/js/src/ui/Balance/balance.spec.js +++ b/js/src/ui/Balance/balance.spec.js @@ -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( , { - 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); }); }); }); diff --git a/js/src/ui/Form/index.js b/js/src/ui/Form/index.js index 21fd2986c..6a003c472 100644 --- a/js/src/ui/Form/index.js +++ b/js/src/ui/Form/index.js @@ -14,35 +14,19 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -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 -}; diff --git a/js/src/ui/TokenImage/index.js b/js/src/ui/TokenImage/index.js new file mode 100644 index 000000000..f0b2f9d49 --- /dev/null +++ b/js/src/ui/TokenImage/index.js @@ -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 . + +export default from './tokenImage'; diff --git a/js/src/ui/TokenImage/tokenImage.js b/js/src/ui/TokenImage/tokenImage.js new file mode 100644 index 000000000..e0e66d22b --- /dev/null +++ b/js/src/ui/TokenImage/tokenImage.js @@ -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 . + +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 ( + { + ); + } +} + +function mapStateToProps (iniState) { + const { images } = iniState; + + return (_, props) => { + const { token } = props; + + return { image: images[token.address] }; + }; +} + +export default connect( + mapStateToProps, + null +)(TokenImage); diff --git a/js/src/ui/TxList/TxRow/txRow.js b/js/src/ui/TxList/TxRow/txRow.js index 10f78307c..e42f64159 100644 --- a/js/src/ui/TxList/TxRow/txRow.js +++ b/js/src/ui/TxList/TxRow/txRow.js @@ -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 = ( - - + ); } @@ -138,4 +139,30 @@ export default class TxRow extends Component { ); } + + 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); + diff --git a/js/src/ui/TxList/TxRow/txRow.spec.js b/js/src/ui/TxList/TxRow/txRow.spec.js index f46f2e7fa..dc9f4d3cc 100644 --- a/js/src/ui/TxList/TxRow/txRow.spec.js +++ b/js/src/ui/TxList/TxRow/txRow.spec.js @@ -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( , { 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'); + }); }); }); diff --git a/js/src/ui/TxList/txList.css b/js/src/ui/TxList/txList.css index 3913fea33..a38ba14fd 100644 --- a/js/src/ui/TxList/txList.css +++ b/js/src/ui/TxList/txList.css @@ -65,6 +65,11 @@ .link { vertical-align: top; + + &.currentLink { + color: white; + cursor: text; + } } .right { diff --git a/js/src/ui/index.js b/js/src/ui/index.js index 41903adf1..0ec1cce53 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -14,118 +14,43 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -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'; diff --git a/js/src/util/signer.js b/js/src/util/signer.js index db90049f1..07a64b527 100644 --- a/js/src/util/signer.js +++ b/js/src/util/signer.js @@ -15,7 +15,7 @@ // along with Parity. If not, see . import scrypt from 'scryptsy'; -import * as Transaction from 'ethereumjs-tx'; +import Transaction from 'ethereumjs-tx'; import { pbkdf2Sync } from 'crypto'; import { createDecipheriv } from 'browserify-aes'; diff --git a/js/src/views/Account/Header/header.spec.js b/js/src/views/Account/Header/header.spec.js index 4881edb37..4f56044f0 100644 --- a/js/src/views/Account/Header/header.spec.js +++ b/js/src/views/Account/Header/header.spec.js @@ -73,7 +73,7 @@ describe('views/Account/Header', () => { beforeEach(() => { render({ balance: { balance: 'testing' } }); - balance = component.find('Connect(Balance)'); + balance = component.find('Balance'); }); it('renders', () => { diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js index c92ce0c77..230ee788a 100644 --- a/js/src/views/Account/account.js +++ b/js/src/views/Account/account.js @@ -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 ( ); @@ -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 }; } diff --git a/js/src/views/Contract/contract.js b/js/src/views/Contract/contract.js index ae6eab84c..1c7d571c5 100644 --- a/js/src/views/Contract/contract.js +++ b/js/src/views/Contract/contract.js @@ -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'; diff --git a/js/src/views/ParityBar/parityBar.js b/js/src/views/ParityBar/parityBar.js index 63036b9e4..ac251d0a3 100644 --- a/js/src/views/ParityBar/parityBar.js +++ b/js/src/views/ParityBar/parityBar.js @@ -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 { /> ) : ( - + ) }
diff --git a/js/src/views/Signer/components/Account/AccountLink/accountLink.js b/js/src/views/Signer/components/Account/AccountLink/accountLink.js index 265642246..81f25f4e1 100644 --- a/js/src/views/Signer/components/Account/AccountLink/accountLink.js +++ b/js/src/views/Signer/components/Account/AccountLink/accountLink.js @@ -15,16 +15,19 @@ // along with Parity. If not, see . 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,36 +35,72 @@ 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 ( + + { children || address } + + ); + } return ( - { children || address } - + ); } - updateLink (address, isTest) { - const link = addressLink(address, isTest); + 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); diff --git a/js/src/views/Signer/components/Account/account.js b/js/src/views/Signer/components/Account/account.js index b2b109634..1a7197d1a 100644 --- a/js/src/views/Signer/components/Account/account.js +++ b/js/src/views/Signer/components/Account/account.js @@ -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 (
; if (!name) { return ( [{ this.shortAddress(address) }] @@ -96,6 +99,7 @@ export default class Account extends Component { return ( diff --git a/js/src/views/Signer/components/SignRequest/signRequest.js b/js/src/views/Signer/components/SignRequest/signRequest.js index c76769763..21f5211e6 100644 --- a/js/src/views/Signer/components/SignRequest/signRequest.js +++ b/js/src/views/Signer/components/SignRequest/signRequest.js @@ -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
; @@ -105,6 +107,7 @@ export default class SignRequest extends Component {
diff --git a/js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.js b/js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.js index 812d1fb35..e73fee922 100644 --- a/js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.js +++ b/js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.js @@ -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 (
@@ -59,6 +60,7 @@ export default class TransactionMainDetails extends Component {
diff --git a/js/src/views/Signer/components/TransactionPending/transactionPending.js b/js/src/views/Signer/components/TransactionPending/transactionPending.js index a49f5c2e0..1a96d0144 100644 --- a/js/src/views/Signer/components/TransactionPending/transactionPending.js +++ b/js/src/views/Signer/components/TransactionPending/transactionPending.js @@ -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 (
); @@ -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, diff --git a/js/src/views/WriteContract/writeContract.js b/js/src/views/WriteContract/writeContract.js index 1ae9a9e53..8fdb55c6f 100644 --- a/js/src/views/WriteContract/writeContract.js +++ b/js/src/views/WriteContract/writeContract.js @@ -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'; diff --git a/js/src/views/index.js b/js/src/views/index.js index b469d6310..69b5d25f9 100644 --- a/js/src/views/index.js +++ b/js/src/views/index.js @@ -14,44 +14,20 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -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'; diff --git a/js/test/npmJsonRpc.js b/js/test/npmJsonRpc.js index a72b367d2..db97d5d8d 100644 --- a/js/test/npmJsonRpc.js +++ b/js/test/npmJsonRpc.js @@ -15,7 +15,7 @@ // along with Parity. If not, see . try { - var JsonRpc = require('../.npmjs/jsonRpc/library.js'); + var JsonRpc = require('../.npmjs/jsonrpc/library.js').default; if (typeof JsonRpc !== 'object') { throw new Error('JsonRpc'); diff --git a/js/webpack/app.js b/js/webpack/app.js index e542b5a2a..1e65a181b 100644 --- a/js/webpack/app.js +++ b/js/webpack/app.js @@ -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,53 +160,63 @@ module.exports = { }); }); - const plugins = Shared.getPlugins().concat( - new CopyWebpackPlugin([ - { from: './error_pages.css', to: 'styles.css' }, - { from: 'dapps/static' } - ], {}), - - new WebpackErrorNotificationPlugin(), - - new webpack.DllReferencePlugin({ - context: '.', - manifest: require(`../${DEST}/vendor-manifest.json`) - }), - - new HtmlWebpackPlugin({ - title: 'Parity', - filename: 'index.html', - template: './index.ejs', - favicon: FAVICON, - chunks: [ - isProd ? null : 'commons', - 'index' - ] - }), - - new HtmlWebpackPlugin({ - title: 'Parity Bar', - filename: 'embed.html', - template: './index.ejs', - favicon: FAVICON, - chunks: [ - isProd ? null : 'commons', - 'embed' - ] - }), - - new ScriptExtHtmlWebpackPlugin({ - sync: [ 'commons', 'vendor.js' ], - defaultAttribute: 'defer' - }), - - new ServiceWorkerWebpackPlugin({ - entry: path.join(__dirname, '../src/serviceWorker.js') - }), - - DappsHTMLInjection + let plugins = Shared.getPlugins().concat( + new WebpackErrorNotificationPlugin() ); + if (!isEmbed) { + plugins = [].concat( + plugins, + + new HtmlWebpackPlugin({ + title: 'Parity', + filename: 'index.html', + template: './index.ejs', + favicon: FAVICON, + chunks: [ + isProd ? null : 'commons', + 'index' + ] + }), + + 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', + template: './index.ejs', + favicon: FAVICON, + chunks: [ + isProd ? null : 'commons', + 'embed' + ] + }) + ); + } + if (!isProd) { const DEST_I18N = path.join(__dirname, '..', DEST, 'i18n'); diff --git a/js/webpack/build.server.js b/js/webpack/build.server.js index 88e8fd193..efc8a2cda 100644 --- a/js/webpack/build.server.js +++ b/js/webpack/build.server.js @@ -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); diff --git a/js/webpack/embed.js b/js/webpack/embed.js new file mode 100644 index 000000000..fd94a5b7c --- /dev/null +++ b/js/webpack/embed.js @@ -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 . + +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'); +}); + diff --git a/js/webpack/shared.js b/js/webpack/shared.js index 15e9f6fc4..80e79a9d2 100644 --- a/js/webpack/shared.js +++ b/js/webpack/shared.js @@ -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), diff --git a/mac/Parity Ethereum.xcodeproj/project.pbxproj b/mac/Parity Ethereum.xcodeproj/project.pbxproj index 0660fee06..913feec70 100644 --- a/mac/Parity Ethereum.xcodeproj/project.pbxproj +++ b/mac/Parity Ethereum.xcodeproj/project.pbxproj @@ -37,7 +37,7 @@ 0ACF9AC61E30FAB600D5C935 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 0ACF9AC81E30FAB600D5C935 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 0AE564F01E3CE42C00BD01F7 /* GetBSDProcessList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetBSDProcessList.swift; sourceTree = ""; }; - 0AED4D9F1E3E22F800BF87C0 /* ethstore */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = ethstore; path = ../target/release/deps/ethstore; sourceTree = ""; }; + 0AED4D9F1E3E22F800BF87C0 /* ethstore */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = ethstore; path = ../target/release/ethstore; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ diff --git a/parity/account.rs b/parity/account.rs index dd5710f33..217bb3401 100644 --- a/parity/account.rs +++ b/parity/account.rs @@ -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 { 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 { fn list(list_cmd: ListAccounts) -> Result { 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)) diff --git a/parity/cli/mod.rs b/parity/cli/mod.rs index 3d1b31457..6e24f639d 100644 --- a/parity/cli/mod.rs +++ b/parity/cli/mod.rs @@ -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>, password: Option>, keys_iterations: Option, + disable_hardware: Option, } #[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, diff --git a/parity/cli/usage.txt b/parity/cli/usage.txt index 9f554eb10..90c207378 100644 --- a/parity/cli/usage.txt +++ b/parity/cli/usage.txt @@ -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, diff --git a/parity/configuration.rs b/parity/configuration.rs index d7d1a5f60..c500f045a 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -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) diff --git a/parity/params.rs b/parity/params.rs index 4464f78eb..4e3256bf1 100644 --- a/parity/params.rs +++ b/parity/params.rs @@ -175,6 +175,7 @@ pub struct AccountsConfig { pub testnet: bool, pub password_files: Vec, pub unlocked_accounts: Vec
, + 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, } } } diff --git a/parity/presale.rs b/parity/presale.rs index 07966606b..5ac7d00fd 100644 --- a/parity/presale.rs +++ b/parity/presale.rs @@ -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 { 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(); diff --git a/parity/run.rs b/parity/run.rs index 9247b254a..56cd26ea0 100644 --- a/parity/run.rs +++ b/parity/run.rs @@ -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 diff --git a/rpc/src/v1/helpers/dispatch.rs b/rpc/src/v1/helpers/dispatch.rs index 0fbe318d4..8900593ac 100644 --- a/rpc/src/v1/helpers/dispatch.rs +++ b/rpc/src/v1/helpers/dispatch.rs @@ -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 Dispatcher for FullDispatcher(client: &C, miner: &M) -> U256 where C: MiningBlockChainClient, M: MinerService { diff --git a/rpc/src/v1/impls/parity.rs b/rpc/src/v1/impls/parity.rs index e872b8dc3..3b2267395 100644 --- a/rpc/src/v1/impls/parity.rs +++ b/rpc/src/v1/impls/parity.rs @@ -45,6 +45,7 @@ use v1::types::{ TransactionStats, LocalTransactionStatus, BlockNumber, ConsensusCapability, VersionInfo, OperationsInfo, DappId, ChainStatus, + AccountInfo, HwAccountInfo }; /// Parity implementation. @@ -111,7 +112,7 @@ impl Parity for ParityClient where { type Metadata = Metadata; - fn accounts_info(&self, dapp: Trailing) -> Result>, Error> { + fn accounts_info(&self, dapp: Trailing) -> Result, Error> { let dapp = dapp.0; let store = take_weak!(self.accounts); @@ -128,12 +129,17 @@ impl Parity for ParityClient 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, 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() ) } diff --git a/rpc/src/v1/tests/mocked/parity_accounts.rs b/rpc/src/v1/tests/mocked/parity_accounts.rs index ad0ead9c0..1c3cd2f8e 100644 --- a/rpc/src/v1/tests/mocked/parity_accounts.rs +++ b/rpc/src/v1/tests/mocked/parity_accounts.rs @@ -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 { fn accounts_provider_with_vaults_support(temp_path: &str) -> Arc { 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) -> 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}"#; diff --git a/rpc/src/v1/traits/parity.rs b/rpc/src/v1/traits/parity.rs index 5119d49b9..d5ecbd5e6 100644 --- a/rpc/src/v1/traits/parity.rs +++ b/rpc/src/v1/traits/parity.rs @@ -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) -> Result>, Error>; + fn accounts_info(&self, Trailing) -> Result, Error>; + + /// Returns hardware accounts information. + #[rpc(name = "parity_hardwareAccountsInfo")] + fn hardware_accounts_info(&self) -> Result, Error>; /// Returns default account for dapp. #[rpc(meta, name = "parity_defaultAccount")] diff --git a/rpc/src/v1/types/account_info.rs b/rpc/src/v1/types/account_info.rs new file mode 100644 index 000000000..077110df0 --- /dev/null +++ b/rpc/src/v1/types/account_info.rs @@ -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 . + +/// 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, +} diff --git a/rpc/src/v1/types/mod.rs.in b/rpc/src/v1/types/mod.rs.in index a823a0104..268b57e4c 100644 --- a/rpc/src/v1/types/mod.rs.in +++ b/rpc/src/v1/types/mod.rs.in @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +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}; diff --git a/util/src/snappy.rs b/util/src/snappy.rs index df532259a..4901e9d9e 100644 --- a/util/src/snappy.rs +++ b/util/src/snappy.rs @@ -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, @@ -154,4 +154,4 @@ pub fn decompress_into(input: &[u8], output: &mut Vec) -> Result bool { let status = unsafe { snappy_validate_compressed_buffer(input.as_ptr() as *const c_char, input.len() as size_t )}; status == SNAPPY_OK -} \ No newline at end of file +}