Merge branch 'lightrpc' into light-txq
This commit is contained in:
commit
2c43b02e13
55
Cargo.lock
generated
55
Cargo.lock
generated
@ -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#c10b1180646c9dc3f23a9b6bb825abcd3b7487ce"
|
||||
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#086ef689513b478463289c5b5845fb6a232f17ec"
|
||||
dependencies = [
|
||||
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@ -2487,6 +2536,7 @@ dependencies = [
|
||||
"checksum aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07d344974f0a155f091948aa389fb1b912d3a58414fbdb9c8d446d193ee3496a"
|
||||
"checksum base32 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1b9605ba46d61df0410d8ac686b0007add8172eba90e8e909c347856fe794d8c"
|
||||
"checksum bigint 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2311bcd71b281e142a095311c22509f0d6bcd87b3000d7dbaa810929b9d6f6ae"
|
||||
"checksum bit-set 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e6e1e6fb1c9e3d6fcdec57216a74eaa03e41f52a22f13a16438251d8e88b89da"
|
||||
"checksum bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9bf6104718e80d7b26a68fdbacff3481cfc05df670821affc7e9cbc1884400c"
|
||||
"checksum bit-vec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5b97c2c8e8bbb4251754f559df8af22fb264853c7d009084a576cdf12565089d"
|
||||
"checksum bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3"
|
||||
@ -2526,6 +2576,7 @@ dependencies = [
|
||||
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
|
||||
"checksum hamming 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65043da274378d68241eb9a8f8f8aa54e349136f7b8e12f63e3ef44043cc30e1"
|
||||
"checksum heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "abb306abb8d398e053cfb1b3e7b72c2f580be048b85745c52652954f8ad1439c"
|
||||
"checksum hidapi 0.3.1 (git+https://github.com/ethcore/hidapi-rs)" = "<none>"
|
||||
"checksum hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2da7d3a34cf6406d9d700111b8eafafe9a251de41ae71d8052748259343b58"
|
||||
"checksum httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "46534074dbb80b070d60a5cb8ecadd8963a00a438ae1a95268850a7ef73b67ae"
|
||||
"checksum hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)" = "<none>"
|
||||
@ -2548,6 +2599,8 @@ dependencies = [
|
||||
"checksum lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "49247ec2a285bb3dcb23cbd9c35193c025e7251bfce77c1d5da97e6362dffe7f"
|
||||
"checksum lazycell 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce12306c4739d86ee97c23139f3a34ddf0387bbf181bc7929d287025a8c3ef6b"
|
||||
"checksum libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)" = "408014cace30ee0f767b1c4517980646a573ec61a57957aeeabcac8ac0a02e8d"
|
||||
"checksum libusb 0.3.0 (git+https://github.com/ethcore/libusb-rs)" = "<none>"
|
||||
"checksum libusb-sys 0.2.3 (git+https://github.com/ethcore/libusb-sys)" = "<none>"
|
||||
"checksum linked-hash-map 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bda158e0dabeb97ee8a401f4d17e479d6b891a14de0bba79d5cc2d4d325b5e48"
|
||||
"checksum linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd"
|
||||
"checksum log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ab83497bf8bf4ed2a74259c1c802351fcd67a65baa86394b6ba73c36f4838054"
|
||||
|
@ -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]
|
||||
|
@ -29,6 +29,7 @@ use ethstore::{SimpleSecretStore, SecretStore, Error as SSError, EthStore, EthMu
|
||||
use ethstore::dir::MemoryDirectory;
|
||||
use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator};
|
||||
use ethjson::misc::AccountMeta;
|
||||
use hardware_wallet::{Error as HardwareError, HardwareWalletManager, KeyPath};
|
||||
pub use ethstore::ethkey::Signature;
|
||||
|
||||
/// Type of unlock.
|
||||
@ -55,6 +56,10 @@ struct AccountData {
|
||||
pub enum SignError {
|
||||
/// Account is not unlocked
|
||||
NotUnlocked,
|
||||
/// Account does not exist.
|
||||
NotFound,
|
||||
/// Low-level hardware device error.
|
||||
Hardware(HardwareError),
|
||||
/// Low-level error from store
|
||||
SStore(SSError)
|
||||
}
|
||||
@ -63,11 +68,19 @@ impl fmt::Display for SignError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
SignError::NotUnlocked => write!(f, "Account is locked"),
|
||||
SignError::NotFound => write!(f, "Account does not exist"),
|
||||
SignError::Hardware(ref e) => write!(f, "{}", e),
|
||||
SignError::SStore(ref e) => write!(f, "{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HardwareError> for SignError {
|
||||
fn from(e: HardwareError) -> Self {
|
||||
SignError::Hardware(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SSError> for SignError {
|
||||
fn from(e: SSError) -> Self {
|
||||
SignError::SStore(e)
|
||||
@ -107,17 +120,47 @@ pub struct AccountProvider {
|
||||
sstore: Box<SecretStore>,
|
||||
/// Accounts unlocked with rolling tokens
|
||||
transient_sstore: EthMultiStore,
|
||||
/// Accounts in hardware wallets.
|
||||
hardware_store: Option<HardwareWalletManager>,
|
||||
}
|
||||
|
||||
/// Account management settings.
|
||||
pub struct AccountProviderSettings {
|
||||
/// Enable hardware wallet support.
|
||||
pub enable_hardware_wallets: bool,
|
||||
/// Use the classic chain key on the hardware wallet.
|
||||
pub hardware_wallet_classic_key: bool,
|
||||
}
|
||||
|
||||
impl Default for AccountProviderSettings {
|
||||
fn default() -> Self {
|
||||
AccountProviderSettings {
|
||||
enable_hardware_wallets: false,
|
||||
hardware_wallet_classic_key: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountProvider {
|
||||
/// Creates new account provider.
|
||||
pub fn new(sstore: Box<SecretStore>) -> Self {
|
||||
pub fn new(sstore: Box<SecretStore>, settings: AccountProviderSettings) -> Self {
|
||||
let mut hardware_store = None;
|
||||
if settings.enable_hardware_wallets {
|
||||
match HardwareWalletManager::new() {
|
||||
Ok(manager) => {
|
||||
manager.set_key_path(if settings.hardware_wallet_classic_key { KeyPath::EthereumClassic } else { KeyPath::Ethereum });
|
||||
hardware_store = Some(manager)
|
||||
},
|
||||
Err(e) => warn!("Error initializing hardware wallets: {}", e),
|
||||
}
|
||||
}
|
||||
AccountProvider {
|
||||
unlocked: RwLock::new(HashMap::new()),
|
||||
address_book: RwLock::new(AddressBook::new(&sstore.local_path())),
|
||||
dapps_settings: RwLock::new(DappsSettingsStore::new(&sstore.local_path())),
|
||||
sstore: sstore,
|
||||
transient_sstore: transient_sstore(),
|
||||
hardware_store: hardware_store,
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,6 +172,7 @@ impl AccountProvider {
|
||||
dapps_settings: RwLock::new(DappsSettingsStore::transient()),
|
||||
sstore: Box::new(EthStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory load always succeeds; qed")),
|
||||
transient_sstore: transient_sstore(),
|
||||
hardware_store: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,6 +220,12 @@ impl AccountProvider {
|
||||
Ok(accounts.into_iter().map(|a| a.address).collect())
|
||||
}
|
||||
|
||||
/// Returns addresses of hardware accounts.
|
||||
pub fn hardware_accounts(&self) -> Result<Vec<Address>, Error> {
|
||||
let accounts = self.hardware_store.as_ref().map_or(Vec::new(), |h| h.list_wallets());
|
||||
Ok(accounts.into_iter().map(|a| a.address).collect())
|
||||
}
|
||||
|
||||
/// Sets a whitelist of accounts exposed for unknown dapps.
|
||||
/// `None` means that all accounts will be visible.
|
||||
pub fn set_new_dapps_whitelist(&self, accounts: Option<Vec<Address>>) -> Result<(), Error> {
|
||||
@ -271,21 +321,43 @@ impl AccountProvider {
|
||||
|
||||
/// Returns each account along with name and meta.
|
||||
pub fn accounts_info(&self) -> Result<HashMap<Address, AccountMeta>, Error> {
|
||||
let r: HashMap<Address, AccountMeta> = self.sstore.accounts()?
|
||||
let r = self.sstore.accounts()?
|
||||
.into_iter()
|
||||
.map(|a| (a.address.clone(), self.account_meta(a.address).ok().unwrap_or_default()))
|
||||
.collect();
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Returns each hardware account along with name and meta.
|
||||
pub fn hardware_accounts_info(&self) -> Result<HashMap<Address, AccountMeta>, Error> {
|
||||
let r = self.hardware_accounts()?
|
||||
.into_iter()
|
||||
.map(|address| (address.clone(), self.account_meta(address).ok().unwrap_or_default()))
|
||||
.collect();
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Returns each hardware account along with name and meta.
|
||||
pub fn is_hardware_address(&self, address: Address) -> bool {
|
||||
self.hardware_store.as_ref().and_then(|s| s.wallet_info(&address)).is_some()
|
||||
}
|
||||
|
||||
/// Returns each account along with name and meta.
|
||||
pub fn account_meta(&self, address: Address) -> Result<AccountMeta, Error> {
|
||||
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<Signature, SignError> {
|
||||
match self.hardware_store.as_ref().map(|s| s.sign_transaction(&address, transaction)) {
|
||||
None | Some(Err(HardwareError::KeyNotFound)) => Err(SignError::NotFound),
|
||||
Some(Err(e)) => Err(From::from(e)),
|
||||
Some(Ok(s)) => Ok(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -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;
|
||||
|
@ -154,7 +154,7 @@ impl Transaction {
|
||||
pub fn hash(&self, network_id: Option<u64>) -> H256 {
|
||||
let mut stream = RlpStream::new();
|
||||
self.rlp_append_unsigned_transaction(&mut stream, network_id);
|
||||
stream.out().sha3()
|
||||
stream.as_raw().sha3()
|
||||
}
|
||||
|
||||
/// Signs the transaction as coming from `sender`.
|
||||
|
@ -21,6 +21,53 @@ use Public;
|
||||
use bigint::hash::{H256, FixedHash};
|
||||
pub use self::derivation::Error as DerivationError;
|
||||
|
||||
/// Represents label that can be stored as a part of key derivation
|
||||
pub trait Label {
|
||||
/// Length of the data that label occupies
|
||||
fn len() -> usize;
|
||||
|
||||
/// Store label data to the key derivation sequence
|
||||
/// Must not use more than `len()` bytes from slice
|
||||
fn store(&self, target: &mut [u8]);
|
||||
}
|
||||
|
||||
impl Label for u32 {
|
||||
fn len() -> usize { 4 }
|
||||
|
||||
fn store(&self, target: &mut [u8]) {
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
|
||||
BigEndian::write_u32(&mut target[0..4], *self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Key derivation over generic label `T`
|
||||
pub enum Derivation<T: Label> {
|
||||
/// Soft key derivation (allow proof of parent)
|
||||
Soft(T),
|
||||
/// Hard key derivation (does not allow proof of parent)
|
||||
Hard(T),
|
||||
}
|
||||
|
||||
impl From<u32> for Derivation<u32> {
|
||||
fn from(index: u32) -> Self {
|
||||
if index < (2 << 30) {
|
||||
Derivation::Soft(index)
|
||||
}
|
||||
else {
|
||||
Derivation::Hard(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Label for H256 {
|
||||
fn len() -> usize { 32 }
|
||||
|
||||
fn store(&self, target: &mut [u8]) {
|
||||
self.copy_to(&mut target[0..32]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extended secret key, allows deterministic derivation of subsequent keys.
|
||||
pub struct ExtendedSecret {
|
||||
secret: Secret,
|
||||
@ -49,7 +96,7 @@ impl ExtendedSecret {
|
||||
}
|
||||
|
||||
/// Derive new private key
|
||||
pub fn derive(&self, index: u32) -> ExtendedSecret {
|
||||
pub fn derive<T>(&self, index: Derivation<T>) -> ExtendedSecret where T: Label {
|
||||
let (derived_key, next_chain_code) = derivation::private(*self.secret, self.chain_code, index);
|
||||
|
||||
let derived_secret = Secret::from_slice(&*derived_key)
|
||||
@ -88,7 +135,7 @@ impl ExtendedPublic {
|
||||
|
||||
/// Derive new public key
|
||||
/// Operation is defined only for index belongs [0..2^31)
|
||||
pub fn derive(&self, index: u32) -> Result<Self, DerivationError> {
|
||||
pub fn derive<T>(&self, index: Derivation<T>) -> Result<Self, DerivationError> where T: Label {
|
||||
let (derived_key, next_chain_code) = derivation::public(self.public, self.chain_code, index)?;
|
||||
Ok(ExtendedPublic::new(derived_key, next_chain_code))
|
||||
}
|
||||
@ -147,7 +194,7 @@ impl ExtendedKeyPair {
|
||||
&self.public
|
||||
}
|
||||
|
||||
pub fn derive(&self, index: u32) -> Result<Self, DerivationError> {
|
||||
pub fn derive<T>(&self, index: Derivation<T>) -> Result<Self, DerivationError> where T: Label {
|
||||
let derived = self.secret.derive(index);
|
||||
|
||||
Ok(ExtendedKeyPair {
|
||||
@ -167,11 +214,11 @@ mod derivation {
|
||||
use rcrypto::sha2::Sha512;
|
||||
use bigint::hash::{H512, H256, FixedHash};
|
||||
use bigint::prelude::{U256, U512, Uint};
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use secp256k1;
|
||||
use secp256k1::key::{SecretKey, PublicKey};
|
||||
use SECP256K1;
|
||||
use keccak;
|
||||
use super::{Label, Derivation};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
@ -183,20 +230,18 @@ mod derivation {
|
||||
|
||||
// Deterministic derivation of the key using secp256k1 elliptic curve.
|
||||
// Derivation can be either hardened or not.
|
||||
// For hardened derivation, pass index at least 2^31
|
||||
// For hardened derivation, pass u32 index at least 2^31 or custom Derivation::Hard(T) enum
|
||||
//
|
||||
// Can panic if passed `private_key` is not a valid secp256k1 private key
|
||||
// (outside of (0..curve_n()]) field
|
||||
pub fn private(private_key: H256, chain_code: H256, index: u32) -> (H256, H256) {
|
||||
if index < (2 << 30) {
|
||||
private_soft(private_key, chain_code, index)
|
||||
}
|
||||
else {
|
||||
private_hard(private_key, chain_code, index)
|
||||
pub fn private<T>(private_key: H256, chain_code: H256, index: Derivation<T>) -> (H256, H256) where T: Label {
|
||||
match index {
|
||||
Derivation::Soft(index) => private_soft(private_key, chain_code, index),
|
||||
Derivation::Hard(index) => private_hard(private_key, chain_code, index),
|
||||
}
|
||||
}
|
||||
|
||||
fn hmac_pair(data: [u8; 37], private_key: H256, chain_code: H256) -> (H256, H256) {
|
||||
fn hmac_pair(data: &[u8], private_key: H256, chain_code: H256) -> (H256, H256) {
|
||||
let private: U256 = private_key.into();
|
||||
|
||||
// produces 512-bit derived hmac (I)
|
||||
@ -216,8 +261,8 @@ mod derivation {
|
||||
|
||||
// Can panic if passed `private_key` is not a valid secp256k1 private key
|
||||
// (outside of (0..curve_n()]) field
|
||||
fn private_soft(private_key: H256, chain_code: H256, index: u32) -> (H256, H256) {
|
||||
let mut data = [0u8; 37];
|
||||
fn private_soft<T>(private_key: H256, chain_code: H256, index: T) -> (H256, H256) where T: Label {
|
||||
let mut data = vec![0u8; 33 + T::len()];
|
||||
|
||||
let sec_private = SecretKey::from_slice(&SECP256K1, &*private_key)
|
||||
.expect("Caller should provide valid private key");
|
||||
@ -226,26 +271,26 @@ mod derivation {
|
||||
let public_serialized = sec_public.serialize_vec(&SECP256K1, true);
|
||||
|
||||
// curve point (compressed public key) -- index
|
||||
// 0.33 -- 33..37
|
||||
// 0.33 -- 33..end
|
||||
data[0..33].copy_from_slice(&public_serialized);
|
||||
BigEndian::write_u32(&mut data[33..37], index);
|
||||
index.store(&mut data[33..]);
|
||||
|
||||
hmac_pair(data, private_key, chain_code)
|
||||
hmac_pair(&data, private_key, chain_code)
|
||||
}
|
||||
|
||||
// Deterministic derivation of the key using secp256k1 elliptic curve
|
||||
// This is hardened derivation and does not allow to associate
|
||||
// corresponding public keys of the original and derived private keys
|
||||
fn private_hard(private_key: H256, chain_code: H256, index: u32) -> (H256, H256) {
|
||||
let mut data = [0u8; 37];
|
||||
fn private_hard<T>(private_key: H256, chain_code: H256, index: T) -> (H256, H256) where T: Label {
|
||||
let mut data: Vec<u8> = vec![0u8; 33 + T::len()];
|
||||
let private: U256 = private_key.into();
|
||||
|
||||
// 0x00 (padding) -- private_key -- index
|
||||
// 0 -- 1..33 -- 33..37
|
||||
// 0 -- 1..33 -- 33..end
|
||||
private.to_big_endian(&mut data[1..33]);
|
||||
BigEndian::write_u32(&mut data[33..37], index);
|
||||
index.store(&mut data[33..(33 + T::len())]);
|
||||
|
||||
hmac_pair(data, private_key, chain_code)
|
||||
hmac_pair(&data, private_key, chain_code)
|
||||
}
|
||||
|
||||
fn private_add(k1: U256, k2: U256) -> U256 {
|
||||
@ -266,11 +311,11 @@ mod derivation {
|
||||
H256::from_slice(&secp256k1::constants::CURVE_ORDER).into()
|
||||
}
|
||||
|
||||
pub fn public(public_key: H512, chain_code: H256, index: u32) -> Result<(H512, H256), Error> {
|
||||
if index >= (2 << 30) {
|
||||
// public derivation is only defined on 'soft' index space [0..2^31)
|
||||
return Err(Error::InvalidHardenedUse)
|
||||
}
|
||||
pub fn public<T>(public_key: H512, chain_code: H256, derivation: Derivation<T>) -> Result<(H512, H256), Error> where T: Label {
|
||||
let index = match derivation {
|
||||
Derivation::Soft(index) => index,
|
||||
Derivation::Hard(_) => { return Err(Error::InvalidHardenedUse); }
|
||||
};
|
||||
|
||||
let mut public_sec_raw = [0u8; 65];
|
||||
public_sec_raw[0] = 4;
|
||||
@ -278,11 +323,11 @@ mod derivation {
|
||||
let public_sec = PublicKey::from_slice(&SECP256K1, &public_sec_raw).map_err(|_| Error::InvalidPoint)?;
|
||||
let public_serialized = public_sec.serialize_vec(&SECP256K1, true);
|
||||
|
||||
let mut data = [0u8; 37];
|
||||
let mut data = vec![0u8; 33 + T::len()];
|
||||
// curve point (compressed public key) -- index
|
||||
// 0.33 -- 33..37
|
||||
// 0.33 -- 33..end
|
||||
data[0..33].copy_from_slice(&public_serialized);
|
||||
BigEndian::write_u32(&mut data[33..37], index);
|
||||
index.store(&mut data[33..(33 + T::len())]);
|
||||
|
||||
// HMAC512SHA produces [derived private(256); new chain code(256)]
|
||||
let mut hmac = Hmac::new(Sha512::new(), &*chain_code);
|
||||
@ -351,7 +396,7 @@ mod tests {
|
||||
use secret::Secret;
|
||||
use std::str::FromStr;
|
||||
use bigint::hash::{H128, H256};
|
||||
use super::derivation;
|
||||
use super::{derivation, Derivation};
|
||||
|
||||
fn master_chain_basic() -> (H256, H256) {
|
||||
let seed = H128::from_str("000102030405060708090a0b0c0d0e0f")
|
||||
@ -375,23 +420,48 @@ mod tests {
|
||||
|
||||
// hardened
|
||||
assert_eq!(&**extended_secret.secret(), &*secret);
|
||||
assert_eq!(&**extended_secret.derive(2147483648).secret(), &"0927453daed47839608e414a3738dfad10aed17c459bbd9ab53f89b026c834b6".into());
|
||||
assert_eq!(&**extended_secret.derive(2147483649).secret(), &"44238b6a29c6dcbe9b401364141ba11e2198c289a5fed243a1c11af35c19dc0f".into());
|
||||
assert_eq!(&**extended_secret.derive(2147483648.into()).secret(), &"0927453daed47839608e414a3738dfad10aed17c459bbd9ab53f89b026c834b6".into());
|
||||
assert_eq!(&**extended_secret.derive(2147483649.into()).secret(), &"44238b6a29c6dcbe9b401364141ba11e2198c289a5fed243a1c11af35c19dc0f".into());
|
||||
|
||||
// normal
|
||||
assert_eq!(&**extended_secret.derive(0).secret(), &"bf6a74e3f7b36fc4c96a1e12f31abc817f9f5904f5a8fc27713163d1f0b713f6".into());
|
||||
assert_eq!(&**extended_secret.derive(1).secret(), &"bd4fca9eb1f9c201e9448c1eecd66e302d68d4d313ce895b8c134f512205c1bc".into());
|
||||
assert_eq!(&**extended_secret.derive(2).secret(), &"86932b542d6cab4d9c65490c7ef502d89ecc0e2a5f4852157649e3251e2a3268".into());
|
||||
assert_eq!(&**extended_secret.derive(0.into()).secret(), &"bf6a74e3f7b36fc4c96a1e12f31abc817f9f5904f5a8fc27713163d1f0b713f6".into());
|
||||
assert_eq!(&**extended_secret.derive(1.into()).secret(), &"bd4fca9eb1f9c201e9448c1eecd66e302d68d4d313ce895b8c134f512205c1bc".into());
|
||||
assert_eq!(&**extended_secret.derive(2.into()).secret(), &"86932b542d6cab4d9c65490c7ef502d89ecc0e2a5f4852157649e3251e2a3268".into());
|
||||
|
||||
let extended_public = ExtendedPublic::from_secret(&extended_secret).expect("Extended public should be created");
|
||||
let derived_public = extended_public.derive(0).expect("First derivation of public should succeed");
|
||||
let derived_public = extended_public.derive(0.into()).expect("First derivation of public should succeed");
|
||||
assert_eq!(&*derived_public.public(), &"f7b3244c96688f92372bfd4def26dc4151529747bab9f188a4ad34e141d47bd66522ff048bc6f19a0a4429b04318b1a8796c000265b4fa200dae5f6dda92dd94".into());
|
||||
|
||||
let keypair = ExtendedKeyPair::with_secret(
|
||||
Secret::from_str("a100df7a048e50ed308ea696dc600215098141cb391e9527329df289f9383f65").unwrap(),
|
||||
064.into(),
|
||||
);
|
||||
assert_eq!(&**keypair.derive(2147483648).expect("Derivation of keypair should succeed").secret().secret(), &"edef54414c03196557cf73774bc97a645c9a1df2164ed34f0c2a78d1375a930c".into());
|
||||
assert_eq!(&**keypair.derive(2147483648u32.into()).expect("Derivation of keypair should succeed").secret().secret(), &"edef54414c03196557cf73774bc97a645c9a1df2164ed34f0c2a78d1375a930c".into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn h256_soft_match() {
|
||||
let secret = Secret::from_str("a100df7a048e50ed308ea696dc600215098141cb391e9527329df289f9383f65").unwrap();
|
||||
let derivation_secret = H256::from_str("51eaf04f9dbbc1417dc97e789edd0c37ecda88bac490434e367ea81b71b7b015").unwrap();
|
||||
|
||||
let extended_secret = ExtendedSecret::with_code(secret.clone(), 0u64.into());
|
||||
let extended_public = ExtendedPublic::from_secret(&extended_secret).expect("Extended public should be created");
|
||||
|
||||
let derived_secret0 = extended_secret.derive(Derivation::Soft(derivation_secret));
|
||||
let derived_public0 = extended_public.derive(Derivation::Soft(derivation_secret)).expect("First derivation of public should succeed");
|
||||
|
||||
let public_from_secret0 = ExtendedPublic::from_secret(&derived_secret0).expect("Extended public should be created");
|
||||
|
||||
assert_eq!(public_from_secret0.public(), derived_public0.public());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn h256_hard() {
|
||||
let secret = Secret::from_str("a100df7a048e50ed308ea696dc600215098141cb391e9527329df289f9383f65").unwrap();
|
||||
let derivation_secret = H256::from_str("51eaf04f9dbbc1417dc97e789edd0c37ecda88bac490434e367ea81b71b7b015").unwrap();
|
||||
let extended_secret = ExtendedSecret::with_code(secret.clone(), 1u64.into());
|
||||
|
||||
assert_eq!(&**extended_secret.derive(Derivation::Hard(derivation_secret)).secret(), &"2bc2d696fb744d77ff813b4a1ef0ad64e1e5188b622c54ba917acc5ebc7c5486".into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -400,8 +470,8 @@ mod tests {
|
||||
let extended_secret = ExtendedSecret::with_code(secret.clone(), 1.into());
|
||||
let extended_public = ExtendedPublic::from_secret(&extended_secret).expect("Extended public should be created");
|
||||
|
||||
let derived_secret0 = extended_secret.derive(0);
|
||||
let derived_public0 = extended_public.derive(0).expect("First derivation of public should succeed");
|
||||
let derived_secret0 = extended_secret.derive(0.into());
|
||||
let derived_public0 = extended_public.derive(0.into()).expect("First derivation of public should succeed");
|
||||
|
||||
let public_from_secret0 = ExtendedPublic::from_secret(&derived_secret0).expect("Extended public should be created");
|
||||
|
||||
@ -429,7 +499,7 @@ mod tests {
|
||||
/// xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7
|
||||
/// H(0)
|
||||
test_extended(
|
||||
|secret| secret.derive(2147483648),
|
||||
|secret| secret.derive(2147483648.into()),
|
||||
H256::from_str("edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea")
|
||||
.expect("Private should be decoded ok")
|
||||
);
|
||||
@ -440,7 +510,7 @@ mod tests {
|
||||
/// xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs
|
||||
/// H(0)/1
|
||||
test_extended(
|
||||
|secret| secret.derive(2147483648).derive(1),
|
||||
|secret| secret.derive(2147483648.into()).derive(1.into()),
|
||||
H256::from_str("3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368")
|
||||
.expect("Private should be decoded ok")
|
||||
);
|
||||
|
@ -22,8 +22,9 @@ use std::{env, process, fs};
|
||||
use std::io::Read;
|
||||
use docopt::Docopt;
|
||||
use ethstore::ethkey::Address;
|
||||
use ethstore::dir::{KeyDirectory, ParityDirectory, DiskDirectory, GethDirectory, DirectoryType};
|
||||
use ethstore::{EthStore, SecretStore, import_accounts, Error, PresaleWallet};
|
||||
use ethstore::dir::{KeyDirectory, ParityDirectory, RootDiskDirectory, GethDirectory, DirectoryType};
|
||||
use ethstore::{EthStore, SimpleSecretStore, SecretStore, import_accounts, Error, PresaleWallet,
|
||||
SecretVaultRef, StoreAccountRef};
|
||||
|
||||
pub const USAGE: &'static str = r#"
|
||||
Ethereum key management.
|
||||
@ -97,7 +98,7 @@ fn key_dir(location: &str) -> Result<Box<KeyDirectory>, Error> {
|
||||
"parity-test" => Box::new(ParityDirectory::create(DirectoryType::Testnet)?),
|
||||
"geth" => Box::new(GethDirectory::create(DirectoryType::Main)?),
|
||||
"geth-test" => Box::new(GethDirectory::create(DirectoryType::Testnet)?),
|
||||
path => Box::new(DiskDirectory::create(path)?),
|
||||
path => Box::new(RootDiskDirectory::create(path)?),
|
||||
};
|
||||
|
||||
Ok(dir)
|
||||
@ -130,16 +131,17 @@ fn execute<S, I>(command: I) -> Result<String, Error> where I: IntoIterator<Item
|
||||
return if args.cmd_insert {
|
||||
let secret = args.arg_secret.parse().map_err(|_| Error::InvalidSecret)?;
|
||||
let password = load_password(&args.arg_password)?;
|
||||
let address = store.insert_account(secret, &password)?;
|
||||
let address = store.insert_account(SecretVaultRef::Root, secret, &password)?;
|
||||
Ok(format!("0x{:?}", address))
|
||||
} else if args.cmd_change_pwd {
|
||||
let address = args.arg_address.parse().map_err(|_| Error::InvalidAccount)?;
|
||||
let old_pwd = load_password(&args.arg_old_pwd)?;
|
||||
let new_pwd = load_password(&args.arg_new_pwd)?;
|
||||
let ok = store.change_password(&address, &old_pwd, &new_pwd).is_ok();
|
||||
let ok = store.change_password(&StoreAccountRef::root(address), &old_pwd, &new_pwd).is_ok();
|
||||
Ok(format!("{}", ok))
|
||||
} else if args.cmd_list {
|
||||
let accounts = store.accounts()?;
|
||||
let accounts: Vec<_> = accounts.into_iter().map(|a| a.address).collect();
|
||||
Ok(format_accounts(&accounts))
|
||||
} else if args.cmd_import {
|
||||
let src = key_dir(&args.flag_src)?;
|
||||
@ -150,23 +152,23 @@ fn execute<S, I>(command: I) -> Result<String, Error> where I: IntoIterator<Item
|
||||
let wallet = PresaleWallet::open(&args.arg_path)?;
|
||||
let password = load_password(&args.arg_password)?;
|
||||
let kp = wallet.decrypt(&password)?;
|
||||
let address = store.insert_account(kp.secret().clone(), &password)?;
|
||||
let address = store.insert_account(SecretVaultRef::Root, kp.secret().clone(), &password)?;
|
||||
Ok(format!("0x{:?}", address))
|
||||
} else if args.cmd_remove {
|
||||
let address = args.arg_address.parse().map_err(|_| Error::InvalidAccount)?;
|
||||
let password = load_password(&args.arg_password)?;
|
||||
let ok = store.remove_account(&address, &password).is_ok();
|
||||
let ok = store.remove_account(&StoreAccountRef::root(address), &password).is_ok();
|
||||
Ok(format!("{}", ok))
|
||||
} else if args.cmd_sign {
|
||||
let address = args.arg_address.parse().map_err(|_| Error::InvalidAccount)?;
|
||||
let message = args.arg_message.parse().map_err(|_| Error::InvalidMessage)?;
|
||||
let password = load_password(&args.arg_password)?;
|
||||
let signature = store.sign(&address, &password, &message)?;
|
||||
let signature = store.sign(&StoreAccountRef::root(address), &password, &message)?;
|
||||
Ok(format!("0x{:?}", signature))
|
||||
} else if args.cmd_public {
|
||||
let address = args.arg_address.parse().map_err(|_| Error::InvalidAccount)?;
|
||||
let password = load_password(&args.arg_password)?;
|
||||
let public = store.public(&address, &password)?;
|
||||
let public = store.public(&StoreAccountRef::root(address), &password)?;
|
||||
Ok(format!("0x{:?}", public))
|
||||
} else {
|
||||
Ok(format!("{}", USAGE))
|
||||
|
@ -234,6 +234,10 @@ impl<T> VaultKeyDirectoryProvider for DiskDirectory<T> where T: KeyFileManager {
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn vault_meta(&self, name: &str) -> Result<String, Error> {
|
||||
VaultDiskDirectory::meta_at(&self.path, name)
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyFileManager for DiskKeyFileManager {
|
||||
@ -242,7 +246,12 @@ impl KeyFileManager for DiskKeyFileManager {
|
||||
Ok(SafeAccount::from_file(key_file, filename))
|
||||
}
|
||||
|
||||
fn write<T>(&self, account: SafeAccount, writer: &mut T) -> Result<(), Error> where T: io::Write {
|
||||
fn write<T>(&self, mut account: SafeAccount, writer: &mut T) -> Result<(), Error> where T: io::Write {
|
||||
// when account is moved back to root directory from vault
|
||||
// => remove vault field from meta
|
||||
account.meta = json::remove_vault_name_from_json_meta(&account.meta)
|
||||
.map_err(|err| Error::Custom(format!("{:?}", err)))?;
|
||||
|
||||
let key_file: json::KeyFile = account.into();
|
||||
key_file.write(writer).map_err(|e| Error::Custom(format!("{:?}", e)))
|
||||
}
|
||||
|
@ -72,6 +72,8 @@ pub trait VaultKeyDirectoryProvider {
|
||||
fn open(&self, name: &str, key: VaultKey) -> Result<Box<VaultKeyDirectory>, Error>;
|
||||
/// List all vaults
|
||||
fn list_vaults(&self) -> Result<Vec<String>, Error>;
|
||||
/// Get vault meta
|
||||
fn vault_meta(&self, name: &str) -> Result<String, Error>;
|
||||
}
|
||||
|
||||
/// Vault directory
|
||||
|
@ -67,11 +67,23 @@ impl VaultDiskDirectory {
|
||||
}
|
||||
|
||||
// check that passed key matches vault file
|
||||
let meta = read_vault_file(&vault_dir_path, &key)?;
|
||||
let meta = read_vault_file(&vault_dir_path, Some(&key))?;
|
||||
|
||||
Ok(DiskDirectory::new(vault_dir_path, VaultKeyFileManager::new(name, key, &meta)))
|
||||
}
|
||||
|
||||
/// Read vault meta without actually opening the vault
|
||||
pub fn meta_at<P>(root: P, name: &str) -> Result<String, Error> where P: AsRef<Path> {
|
||||
// check that vault directory exists
|
||||
let vault_dir_path = make_vault_dir_path(root, name, true)?;
|
||||
if !vault_dir_path.is_dir() {
|
||||
return Err(Error::VaultNotFound);
|
||||
}
|
||||
|
||||
// check that passed key matches vault file
|
||||
read_vault_file(&vault_dir_path, None)
|
||||
}
|
||||
|
||||
fn create_temp_vault(&self, key: VaultKey) -> Result<VaultDiskDirectory, Error> {
|
||||
let original_path = self.path().expect("self is instance of DiskDirectory; DiskDirectory always returns path; qed");
|
||||
let mut path: PathBuf = original_path.clone();
|
||||
@ -241,7 +253,7 @@ fn create_vault_file<P>(vault_dir_path: P, key: &VaultKey, meta: &str) -> Result
|
||||
}
|
||||
|
||||
/// When vault is opened => we must check that password matches && read metadata
|
||||
fn read_vault_file<P>(vault_dir_path: P, key: &VaultKey) -> Result<String, Error> where P: AsRef<Path> {
|
||||
fn read_vault_file<P>(vault_dir_path: P, key: Option<&VaultKey>) -> Result<String, Error> where P: AsRef<Path> {
|
||||
let mut vault_file_path: PathBuf = vault_dir_path.as_ref().into();
|
||||
vault_file_path.push(VAULT_FILE_NAME);
|
||||
|
||||
@ -250,10 +262,12 @@ fn read_vault_file<P>(vault_dir_path: P, key: &VaultKey) -> Result<String, Error
|
||||
let vault_file_meta = vault_file_contents.meta.unwrap_or("{}".to_owned());
|
||||
let vault_file_crypto: Crypto = vault_file_contents.crypto.into();
|
||||
|
||||
let password_bytes = vault_file_crypto.decrypt(&key.password)?;
|
||||
let password_hash = key.password.sha3();
|
||||
if &*password_hash != password_bytes.as_slice() {
|
||||
return Err(Error::InvalidPassword);
|
||||
if let Some(key) = key {
|
||||
let password_bytes = vault_file_crypto.decrypt(&key.password)?;
|
||||
let password_hash = key.password.sha3();
|
||||
if &*password_hash != password_bytes.as_slice() {
|
||||
return Err(Error::InvalidPassword);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(vault_file_meta)
|
||||
@ -264,7 +278,7 @@ mod test {
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use dir::{VaultKey, VaultKeyDirectory};
|
||||
use dir::VaultKey;
|
||||
use super::{VAULT_FILE_NAME, check_vault_name, make_vault_dir_path, create_vault_file, read_vault_file, VaultDiskDirectory};
|
||||
use devtools::RandomTempPath;
|
||||
|
||||
@ -333,7 +347,7 @@ mod test {
|
||||
}
|
||||
|
||||
// when
|
||||
let result = read_vault_file(&dir, &key);
|
||||
let result = read_vault_file(&dir, Some(&key));
|
||||
|
||||
// then
|
||||
assert!(result.is_ok());
|
||||
@ -349,7 +363,7 @@ mod test {
|
||||
vault_file_path.push(VAULT_FILE_NAME);
|
||||
|
||||
// when
|
||||
let result = read_vault_file(&dir, &key);
|
||||
let result = read_vault_file(&dir, Some(&key));
|
||||
|
||||
// then
|
||||
assert!(result.is_err());
|
||||
@ -362,7 +376,7 @@ mod test {
|
||||
}
|
||||
|
||||
// when
|
||||
let result = read_vault_file(&dir, &key);
|
||||
let result = read_vault_file(&dir, Some(&key));
|
||||
|
||||
// then
|
||||
assert!(result.is_err());
|
||||
@ -418,22 +432,4 @@ mod test {
|
||||
// then
|
||||
assert!(vault.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vault_directory_can_preserve_meta() {
|
||||
// given
|
||||
let temp_path = RandomTempPath::new();
|
||||
let key = VaultKey::new("password", 1024);
|
||||
let dir: PathBuf = temp_path.as_path().into();
|
||||
let vault = VaultDiskDirectory::create(&dir, "vault", key.clone()).unwrap();
|
||||
|
||||
// then
|
||||
assert_eq!(vault.meta(), "{}".to_owned());
|
||||
assert!(vault.set_meta("Hello, world!!!").is_ok());
|
||||
assert_eq!(vault.meta(), "Hello, world!!!".to_owned());
|
||||
|
||||
// and when
|
||||
let vault = VaultDiskDirectory::at(&dir, "vault", key.clone()).unwrap();
|
||||
assert_eq!(vault.meta(), "Hello, world!!!".to_owned());
|
||||
}
|
||||
}
|
||||
|
@ -501,10 +501,17 @@ impl SimpleSecretStore for EthMultiStore {
|
||||
}
|
||||
|
||||
fn get_vault_meta(&self, name: &str) -> Result<String, Error> {
|
||||
// vault meta contains password hint
|
||||
// => allow reading meta even if vault is not yet opened
|
||||
self.vaults.lock()
|
||||
.get(name)
|
||||
.and_then(|v| Some(v.meta()))
|
||||
.ok_or(Error::VaultNotFound)
|
||||
.and_then(|v| Ok(v.meta()))
|
||||
.or_else(|_| {
|
||||
let vault_provider = self.dir.as_vault_provider().ok_or(Error::VaultsAreNotSupported)?;
|
||||
vault_provider.vault_meta(name)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
fn set_vault_meta(&self, name: &str, meta: &str) -> Result<(), Error> {
|
||||
@ -861,4 +868,34 @@ mod tests {
|
||||
assert!(opened_vaults.iter().any(|v| &*v == name1));
|
||||
assert!(opened_vaults.iter().any(|v| &*v == name3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_manage_vaults_meta() {
|
||||
// given
|
||||
let mut dir = RootDiskDirectoryGuard::new();
|
||||
let store = EthStore::open(dir.key_dir.take().unwrap()).unwrap();
|
||||
let name1 = "vault1"; let password1 = "password1";
|
||||
|
||||
// when
|
||||
store.create_vault(name1, password1).unwrap();
|
||||
|
||||
// then
|
||||
assert_eq!(store.get_vault_meta(name1).unwrap(), "{}".to_owned());
|
||||
assert!(store.set_vault_meta(name1, "Hello, world!!!").is_ok());
|
||||
assert_eq!(store.get_vault_meta(name1).unwrap(), "Hello, world!!!".to_owned());
|
||||
|
||||
// and when
|
||||
store.close_vault(name1).unwrap();
|
||||
store.open_vault(name1, password1).unwrap();
|
||||
|
||||
// then
|
||||
assert_eq!(store.get_vault_meta(name1).unwrap(), "Hello, world!!!".to_owned());
|
||||
|
||||
// and when
|
||||
store.close_vault(name1).unwrap();
|
||||
|
||||
// then
|
||||
assert_eq!(store.get_vault_meta(name1).unwrap(), "Hello, world!!!".to_owned());
|
||||
assert!(store.get_vault_meta("vault2").is_err());
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ impl Visitor for VaultFileVisitor {
|
||||
loop {
|
||||
match visitor.visit_key()? {
|
||||
Some(VaultFileField::Crypto) => { crypto = Some(visitor.visit_value()?); },
|
||||
Some(VaultFileField::Meta) => { meta = Some(visitor.visit_value()?); }
|
||||
Some(VaultFileField::Meta) => { meta = visitor.visit_value().ok(); }, // meta is optional
|
||||
None => { break; },
|
||||
}
|
||||
}
|
||||
@ -141,4 +141,29 @@ mod test {
|
||||
|
||||
assert_eq!(file, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_and_from_json_no_meta() {
|
||||
let file = VaultFile {
|
||||
crypto: Crypto {
|
||||
cipher: Cipher::Aes128Ctr(Aes128Ctr {
|
||||
iv: "0155e3690be19fbfbecabcd440aa284b".into(),
|
||||
}),
|
||||
ciphertext: "4d6938a1f49b7782".into(),
|
||||
kdf: Kdf::Pbkdf2(Pbkdf2 {
|
||||
c: 1024,
|
||||
dklen: 32,
|
||||
prf: Prf::HmacSha256,
|
||||
salt: "b6a9338a7ccd39288a86dba73bfecd9101b4f3db9c9830e7c76afdbd4f6872e5".into(),
|
||||
}),
|
||||
mac: "16381463ea11c6eb2239a9f339c2e780516d29d234ce30ac5f166f9080b5a262".into(),
|
||||
},
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_string(&file).unwrap();
|
||||
let deserialized = serde_json::from_str(&serialized).unwrap();
|
||||
|
||||
assert_eq!(file, deserialized);
|
||||
}
|
||||
}
|
||||
|
18
hw/Cargo.toml
Normal file
18
hw/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
description = "Hardware wallet support."
|
||||
homepage = "http://parity.io"
|
||||
license = "GPL-3.0"
|
||||
name = "hardware-wallet"
|
||||
version = "1.6.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
|
||||
[dependencies]
|
||||
log = "0.3"
|
||||
parking_lot = "0.3"
|
||||
hidapi = { git = "https://github.com/ethcore/hidapi-rs" }
|
||||
libusb = { git = "https://github.com/ethcore/libusb-rs" }
|
||||
ethkey = { path = "../ethkey" }
|
||||
ethcore-bigint = { path = "../util/bigint" }
|
||||
|
||||
[dev-dependencies]
|
||||
rustc-serialize = "0.3"
|
364
hw/src/ledger.rs
Normal file
364
hw/src/ledger.rs
Normal file
File diff suppressed because one or more lines are too long
183
hw/src/lib.rs
Normal file
183
hw/src/lib.rs
Normal file
@ -0,0 +1,183 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Hardware wallet management.
|
||||
|
||||
extern crate parking_lot;
|
||||
extern crate hidapi;
|
||||
extern crate libusb;
|
||||
extern crate ethkey;
|
||||
extern crate ethcore_bigint;
|
||||
#[macro_use] extern crate log;
|
||||
#[cfg(test)] extern crate rustc_serialize;
|
||||
|
||||
mod ledger;
|
||||
|
||||
use std::fmt;
|
||||
use std::thread;
|
||||
use std::sync::atomic;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::time::Duration;
|
||||
use parking_lot::Mutex;
|
||||
use ethkey::{Address, Signature};
|
||||
|
||||
pub use ledger::KeyPath;
|
||||
|
||||
/// Hardware waller error.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Ledger device error.
|
||||
LedgerDevice(ledger::Error),
|
||||
/// USB error.
|
||||
Usb(libusb::Error),
|
||||
/// Hardware wallet not found for specified key.
|
||||
KeyNotFound,
|
||||
}
|
||||
|
||||
/// Hardware waller information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WalletInfo {
|
||||
/// Wallet device name.
|
||||
pub name: String,
|
||||
/// Wallet device manufacturer.
|
||||
pub manufacturer: String,
|
||||
/// Wallet device serial number.
|
||||
pub serial: String,
|
||||
/// Ethereum address.
|
||||
pub address: Address,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
Error::KeyNotFound => write!(f, "Key not found for given address."),
|
||||
Error::LedgerDevice(ref e) => write!(f, "{}", e),
|
||||
Error::Usb(ref e) => write!(f, "{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ledger::Error> for Error {
|
||||
fn from(err: ledger::Error) -> Error {
|
||||
match err {
|
||||
ledger::Error::KeyNotFound => Error::KeyNotFound,
|
||||
_ => Error::LedgerDevice(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<libusb::Error> for Error {
|
||||
fn from(err: libusb::Error) -> Error {
|
||||
Error::Usb(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Hardware wallet management interface.
|
||||
pub struct HardwareWalletManager {
|
||||
update_thread: Option<thread::JoinHandle<()>>,
|
||||
exiting: Arc<AtomicBool>,
|
||||
ledger: Arc<Mutex<ledger::Manager>>,
|
||||
}
|
||||
|
||||
struct EventHandler {
|
||||
ledger: Weak<Mutex<ledger::Manager>>,
|
||||
}
|
||||
|
||||
impl libusb::Hotplug for EventHandler {
|
||||
fn device_arrived(&mut self, _device: libusb::Device) {
|
||||
debug!("USB Device arrived");
|
||||
if let Some(l) = self.ledger.upgrade() {
|
||||
for _ in 0..10 {
|
||||
// The device might not be visible right away. Try a few times.
|
||||
if l.lock().update_devices().unwrap_or_else(|e| {
|
||||
debug!("Error enumerating Ledger devices: {}", e);
|
||||
0
|
||||
}) > 0 {
|
||||
break;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn device_left(&mut self, _device: libusb::Device) {
|
||||
debug!("USB Device lost");
|
||||
if let Some(l) = self.ledger.upgrade() {
|
||||
if let Err(e) = l.lock().update_devices() {
|
||||
debug!("Error enumerating Ledger devices: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HardwareWalletManager {
|
||||
pub fn new() -> Result<HardwareWalletManager, Error> {
|
||||
let usb_context = Arc::new(libusb::Context::new()?);
|
||||
let ledger = Arc::new(Mutex::new(ledger::Manager::new()?));
|
||||
usb_context.register_callback(None, None, None, Box::new(EventHandler { ledger: Arc::downgrade(&ledger) }))?;
|
||||
let exiting = Arc::new(AtomicBool::new(false));
|
||||
let thread_exiting = exiting.clone();
|
||||
let l = ledger.clone();
|
||||
let thread = thread::Builder::new().name("hw_wallet".to_string()).spawn(move || {
|
||||
if let Err(e) = l.lock().update_devices() {
|
||||
debug!("Error updating ledger devices: {}", e);
|
||||
}
|
||||
loop {
|
||||
usb_context.handle_events(Some(Duration::from_millis(500))).unwrap_or_else(|e| debug!("Error processing USB events: {}", e));
|
||||
if thread_exiting.load(atomic::Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).ok();
|
||||
Ok(HardwareWalletManager {
|
||||
update_thread: thread,
|
||||
exiting: exiting,
|
||||
ledger: ledger,
|
||||
})
|
||||
}
|
||||
|
||||
/// Select key derivation path for a chain.
|
||||
pub fn set_key_path(&self, key_path: KeyPath) {
|
||||
self.ledger.lock().set_key_path(key_path);
|
||||
}
|
||||
|
||||
|
||||
/// List connected wallets. This only returns wallets that are ready to be used.
|
||||
pub fn list_wallets(&self) -> Vec<WalletInfo> {
|
||||
self.ledger.lock().list_devices()
|
||||
}
|
||||
|
||||
/// Get connected wallet info.
|
||||
pub fn wallet_info(&self, address: &Address) -> Option<WalletInfo> {
|
||||
self.ledger.lock().device_info(address)
|
||||
}
|
||||
|
||||
/// Sign transaction data with wallet managing `address`.
|
||||
pub fn sign_transaction(&self, address: &Address, data: &[u8]) -> Result<Signature, Error> {
|
||||
Ok(self.ledger.lock().sign_transaction(address, data)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HardwareWalletManager {
|
||||
fn drop(&mut self) {
|
||||
self.exiting.store(true, atomic::Ordering::Release);
|
||||
if let Some(thread) = self.update_thread.take() {
|
||||
thread.thread().unpark();
|
||||
thread.join().ok();
|
||||
}
|
||||
}
|
||||
}
|
@ -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" } ]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parity.js",
|
||||
"version": "0.3.72",
|
||||
"version": "0.3.78",
|
||||
"main": "release/index.js",
|
||||
"jsnext:main": "src/index.js",
|
||||
"author": "Parity Team <admin@parity.io>",
|
||||
@ -25,18 +25,20 @@
|
||||
"Promise"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm run build:lib && npm run build:dll && npm run build:app",
|
||||
"build": "npm run build:lib && npm run build:dll && npm run build:app && npm run build:embed",
|
||||
"build:app": "webpack --config webpack/app",
|
||||
"build:lib": "webpack --config webpack/libraries",
|
||||
"build:dll": "webpack --config webpack/vendor",
|
||||
"build:markdown": "babel-node ./scripts/build-rpc-markdown.js",
|
||||
"build:json": "babel-node ./scripts/build-rpc-json.js",
|
||||
"ci:build": "npm run ci:build:lib && npm run ci:build:dll && npm run ci:build:app",
|
||||
"build:embed": "EMBED=1 node webpack/embed",
|
||||
"ci:build": "npm run ci:build:lib && npm run ci:build:dll && npm run ci:build:app && npm run ci:build:embed",
|
||||
"ci:build:app": "NODE_ENV=production webpack --config webpack/app",
|
||||
"ci:build:lib": "NODE_ENV=production webpack --config webpack/libraries",
|
||||
"ci:build:dll": "NODE_ENV=production webpack --config webpack/vendor",
|
||||
"ci:build:npm": "NODE_ENV=production webpack --config webpack/npm",
|
||||
"ci:build:jsonrpc": "babel-node ./scripts/build-rpc-json.js --output .npmjs/jsonrpc",
|
||||
"ci:build:embed": "NODE_ENV=production EMBED=1 node webpack/embed",
|
||||
"start": "npm install && npm run build:lib && npm run build:dll && npm run start:app",
|
||||
"start:app": "node webpack/dev.server",
|
||||
"clean": "rm -rf ./.build ./.coverage ./.happypack ./.npmjs ./build",
|
||||
@ -53,27 +55,28 @@
|
||||
"prepush": "npm run lint:cached"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "6.18.0",
|
||||
"babel-core": "6.21.0",
|
||||
"babel-cli": "6.22.2",
|
||||
"babel-core": "6.22.1",
|
||||
"babel-eslint": "7.1.1",
|
||||
"babel-loader": "6.2.10",
|
||||
"babel-plugin-lodash": "3.2.11",
|
||||
"babel-plugin-react-intl": "2.2.0",
|
||||
"babel-plugin-transform-class-properties": "6.19.0",
|
||||
"babel-plugin-react-intl": "2.3.1",
|
||||
"babel-plugin-recharts": "1.1.0",
|
||||
"babel-plugin-transform-class-properties": "6.22.0",
|
||||
"babel-plugin-transform-decorators-legacy": "1.3.4",
|
||||
"babel-plugin-transform-object-rest-spread": "6.20.2",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.2.11",
|
||||
"babel-plugin-transform-runtime": "6.15.0",
|
||||
"babel-plugin-transform-object-rest-spread": "6.22.0",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.3.0",
|
||||
"babel-plugin-transform-runtime": "6.22.0",
|
||||
"babel-plugin-webpack-alias": "2.1.2",
|
||||
"babel-polyfill": "6.20.0",
|
||||
"babel-preset-env": "1.1.4",
|
||||
"babel-preset-es2015": "6.18.0",
|
||||
"babel-preset-es2016": "6.16.0",
|
||||
"babel-preset-es2017": "6.16.0",
|
||||
"babel-preset-react": "6.16.0",
|
||||
"babel-preset-stage-0": "6.16.0",
|
||||
"babel-register": "6.18.0",
|
||||
"babel-runtime": "6.20.0",
|
||||
"babel-polyfill": "6.22.0",
|
||||
"babel-preset-env": "1.1.8",
|
||||
"babel-preset-es2015": "6.22.0",
|
||||
"babel-preset-es2016": "6.22.0",
|
||||
"babel-preset-es2017": "6.22.0",
|
||||
"babel-preset-react": "6.22.0",
|
||||
"babel-preset-stage-0": "6.22.0",
|
||||
"babel-register": "6.22.0",
|
||||
"babel-runtime": "6.22.0",
|
||||
"chai": "3.5.0",
|
||||
"chai-as-promised": "6.0.0",
|
||||
"chai-enzyme": "0.6.1",
|
||||
@ -132,7 +135,7 @@
|
||||
"stylelint": "7.7.0",
|
||||
"stylelint-config-standard": "15.0.1",
|
||||
"url-loader": "0.5.7",
|
||||
"webpack": "2.2.0-rc.2",
|
||||
"webpack": "2.2.1",
|
||||
"webpack-dev-middleware": "1.9.0",
|
||||
"webpack-error-notification": "0.1.6",
|
||||
"webpack-hot-middleware": "2.14.0",
|
||||
|
@ -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 || {};
|
||||
}
|
||||
|
@ -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({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -15,7 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { inAddress, inAddresses, inData, inHex, inNumber16, inOptions, inBlockNumber } from '../../format/input';
|
||||
import { outAccountInfo, outAddress, outAddresses, outChainStatus, outHistogram, outNumber, outPeers, outTransaction } from '../../format/output';
|
||||
import { outAccountInfo, outAddress, outAddresses, outChainStatus, outHistogram, outNumber, outPeers, outTransaction, outVaultMeta } from '../../format/output';
|
||||
|
||||
export default class Parity {
|
||||
constructor (transport) {
|
||||
@ -55,11 +55,26 @@ export default class Parity {
|
||||
.execute('parity_changePassword', inAddress(account), password, newPassword);
|
||||
}
|
||||
|
||||
changeVault (account, vaultName) {
|
||||
return this._transport
|
||||
.execute('parity_changeVault', inAddress(account), vaultName);
|
||||
}
|
||||
|
||||
changeVaultPassword (vaultName, password) {
|
||||
return this._transport
|
||||
.execute('parity_changeVaultPassword', vaultName, password);
|
||||
}
|
||||
|
||||
checkRequest (requestId) {
|
||||
return this._transport
|
||||
.execute('parity_checkRequest', inNumber16(requestId));
|
||||
}
|
||||
|
||||
closeVault (vaultName) {
|
||||
return this._transport
|
||||
.execute('parity_closeVault', vaultName);
|
||||
}
|
||||
|
||||
consensusCapability () {
|
||||
return this._transport
|
||||
.execute('parity_consensusCapability');
|
||||
@ -167,6 +182,12 @@ export default class Parity {
|
||||
.then((addresses) => addresses ? addresses.map(outAddress) : null);
|
||||
}
|
||||
|
||||
getVaultMeta (vaultName) {
|
||||
return this._transport
|
||||
.execute('parity_getVaultMeta', vaultName)
|
||||
.then(outVaultMeta);
|
||||
}
|
||||
|
||||
hashContent (url) {
|
||||
return this._transport
|
||||
.execute('parity_hashContent', url);
|
||||
@ -189,6 +210,16 @@ export default class Parity {
|
||||
.then((accounts) => (accounts || []).map(outAddress));
|
||||
}
|
||||
|
||||
listOpenedVaults () {
|
||||
return this._transport
|
||||
.execute('parity_listOpenedVaults');
|
||||
}
|
||||
|
||||
listVaults () {
|
||||
return this._transport
|
||||
.execute('parity_listVaults');
|
||||
}
|
||||
|
||||
listRecentDapps () {
|
||||
return this._transport
|
||||
.execute('parity_listRecentDapps');
|
||||
@ -275,6 +306,11 @@ export default class Parity {
|
||||
.then(outAddress);
|
||||
}
|
||||
|
||||
newVault (vaultName, password) {
|
||||
return this._transport
|
||||
.execute('parity_newVault', vaultName, password);
|
||||
}
|
||||
|
||||
nextNonce (account) {
|
||||
return this._transport
|
||||
.execute('parity_nextNonce', inAddress(account))
|
||||
@ -286,6 +322,11 @@ export default class Parity {
|
||||
.execute('parity_nodeName');
|
||||
}
|
||||
|
||||
openVault (vaultName, password) {
|
||||
return this._transport
|
||||
.execute('parity_openVault', vaultName, password);
|
||||
}
|
||||
|
||||
pendingTransactions () {
|
||||
return this._transport
|
||||
.execute('parity_pendingTransactions')
|
||||
@ -399,6 +440,11 @@ export default class Parity {
|
||||
.execute('parity_setTransactionsLimit', inNumber16(quantity));
|
||||
}
|
||||
|
||||
setVaultMeta (vaultName, meta) {
|
||||
return this._transport
|
||||
.execute('parity_setVaultMeta', vaultName, JSON.stringify(meta));
|
||||
}
|
||||
|
||||
signerPort () {
|
||||
return this._transport
|
||||
.execute('parity_signerPort')
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
|
||||
|
@ -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) => {
|
||||
|
@ -49,7 +49,7 @@
|
||||
}
|
||||
|
||||
.token-container {
|
||||
flex: 1;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.full-width .token-container {
|
||||
|
@ -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 {
|
||||
|
@ -16,5 +16,6 @@
|
||||
|
||||
export default {
|
||||
de: 'Deutsch',
|
||||
en: 'English'
|
||||
en: 'English',
|
||||
nl: 'Nederlands'
|
||||
};
|
||||
|
21
js/src/i18n/nl/index.js
Normal file
21
js/src/i18n/nl/index.js
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import settings from './settings';
|
||||
|
||||
export default {
|
||||
settings
|
||||
};
|
63
js/src/i18n/nl/settings.js
Normal file
63
js/src/i18n/nl/settings.js
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
export default {
|
||||
label: 'Instellingen',
|
||||
|
||||
background: {
|
||||
label: 'Achtergrond'
|
||||
},
|
||||
|
||||
parity: {
|
||||
label: 'Parity'
|
||||
},
|
||||
|
||||
proxy: {
|
||||
label: 'Proxy'
|
||||
},
|
||||
|
||||
views: {
|
||||
label: 'Weergaven',
|
||||
|
||||
accounts: {
|
||||
label: 'Accounts'
|
||||
},
|
||||
|
||||
addresses: {
|
||||
label: 'Adresboek'
|
||||
},
|
||||
|
||||
apps: {
|
||||
label: 'Applicaties'
|
||||
},
|
||||
|
||||
contracts: {
|
||||
label: 'Contracten'
|
||||
},
|
||||
|
||||
status: {
|
||||
label: 'Status'
|
||||
},
|
||||
|
||||
signer: {
|
||||
label: 'Signer'
|
||||
},
|
||||
|
||||
settings: {
|
||||
label: 'Instellingen'
|
||||
}
|
||||
}
|
||||
};
|
@ -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',
|
||||
|
@ -31,4 +31,4 @@ if (isNode) {
|
||||
|
||||
import Etherscan from './3rdparty/etherscan';
|
||||
|
||||
module.exports = Etherscan;
|
||||
export default Etherscan;
|
||||
|
@ -16,4 +16,4 @@
|
||||
|
||||
import JsonRpc from './jsonrpc';
|
||||
|
||||
module.exports = JsonRpc;
|
||||
export default JsonRpc;
|
||||
|
@ -32,4 +32,4 @@ if (isNode) {
|
||||
import Abi from './abi';
|
||||
import Api from './api';
|
||||
|
||||
module.exports = { Api, Abi };
|
||||
export { Api, Abi };
|
||||
|
@ -31,4 +31,4 @@ if (isNode) {
|
||||
|
||||
import ShapeShift from './3rdparty/shapeshift';
|
||||
|
||||
module.exports = ShapeShift;
|
||||
export default ShapeShift;
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -16,7 +16,6 @@
|
||||
*/
|
||||
|
||||
.body {
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
@ -14,15 +14,13 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import BigNumber from 'bignumber.js';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { Checkbox, MenuItem } from 'material-ui';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Checkbox } from 'material-ui';
|
||||
|
||||
import Form, { Input, InputAddressSelect, AddressSelect, Select } from '~/ui/Form';
|
||||
import Form, { Input, InputAddressSelect, AddressSelect } from '~/ui/Form';
|
||||
import { nullableProptype } from '~/util/proptypes';
|
||||
|
||||
import imageUnknown from '../../../../assets/images/contracts/unknown-64x64.png';
|
||||
import TokenSelect from './tokenSelect';
|
||||
import styles from '../transfer.css';
|
||||
|
||||
const CHECK_STYLE = {
|
||||
@ -31,110 +29,12 @@ const CHECK_STYLE = {
|
||||
left: '1em'
|
||||
};
|
||||
|
||||
class TokenSelect extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
balance: PropTypes.object.isRequired,
|
||||
images: PropTypes.object.isRequired,
|
||||
tag: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.computeTokens();
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const prevTokens = this.props.balance.tokens.map((t) => `${t.token.tag}_${t.value.toNumber()}`);
|
||||
const nextTokens = nextProps.balance.tokens.map((t) => `${t.token.tag}_${t.value.toNumber()}`);
|
||||
|
||||
if (!isEqual(prevTokens, nextTokens)) {
|
||||
this.computeTokens(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
computeTokens (props = this.props) {
|
||||
const { api } = this.context;
|
||||
const { balance, images } = this.props;
|
||||
|
||||
const items = balance.tokens
|
||||
.filter((token, index) => !index || token.value.gt(0))
|
||||
.map((balance, index) => {
|
||||
const token = balance.token;
|
||||
const isEth = index === 0;
|
||||
let imagesrc = token.image;
|
||||
|
||||
if (!imagesrc) {
|
||||
imagesrc =
|
||||
images[token.address]
|
||||
? `${api.dappsUrl}${images[token.address]}`
|
||||
: imageUnknown;
|
||||
}
|
||||
let value = 0;
|
||||
|
||||
if (isEth) {
|
||||
value = api.util.fromWei(balance.value).toFormat(3);
|
||||
} else {
|
||||
const format = balance.token.format || 1;
|
||||
const decimals = format === 1 ? 0 : Math.min(3, Math.floor(format / 10));
|
||||
|
||||
value = new BigNumber(balance.value).div(format).toFormat(decimals);
|
||||
}
|
||||
|
||||
const label = (
|
||||
<div className={ styles.token }>
|
||||
<img src={ imagesrc } />
|
||||
<div className={ styles.tokenname }>
|
||||
{ token.name }
|
||||
</div>
|
||||
<div className={ styles.tokenbalance }>
|
||||
{ value }<small> { token.tag }</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={ `${index}_${token.tag}` }
|
||||
value={ token.tag }
|
||||
label={ label }
|
||||
>
|
||||
{ label }
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
this.setState({ items });
|
||||
}
|
||||
|
||||
render () {
|
||||
const { tag, onChange } = this.props;
|
||||
const { items } = this.state;
|
||||
|
||||
return (
|
||||
<Select
|
||||
className={ styles.tokenSelect }
|
||||
label='type of token transfer'
|
||||
hint='type of token to transfer'
|
||||
value={ tag }
|
||||
onChange={ onChange }
|
||||
>
|
||||
{ items }
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class Details extends Component {
|
||||
static propTypes = {
|
||||
address: PropTypes.string,
|
||||
balance: PropTypes.object,
|
||||
all: PropTypes.bool,
|
||||
extras: PropTypes.bool,
|
||||
images: PropTypes.object.isRequired,
|
||||
sender: PropTypes.string,
|
||||
senderError: PropTypes.string,
|
||||
sendersBalances: PropTypes.object,
|
||||
@ -249,12 +149,11 @@ export default class Details extends Component {
|
||||
}
|
||||
|
||||
renderTokenSelect () {
|
||||
const { balance, images, tag } = this.props;
|
||||
const { balance, tag } = this.props;
|
||||
|
||||
return (
|
||||
<TokenSelect
|
||||
balance={ balance }
|
||||
images={ images }
|
||||
tag={ tag }
|
||||
onChange={ this.onChangeToken }
|
||||
/>
|
||||
|
114
js/src/modals/Transfer/Details/tokenSelect.js
Normal file
114
js/src/modals/Transfer/Details/tokenSelect.js
Normal file
@ -0,0 +1,114 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import BigNumber from 'bignumber.js';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { MenuItem } from 'material-ui';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { Select } from '~/ui/Form';
|
||||
import TokenImage from '~/ui/TokenImage';
|
||||
|
||||
import styles from '../transfer.css';
|
||||
|
||||
export default class TokenSelect extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
balance: PropTypes.object.isRequired,
|
||||
tag: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.computeTokens();
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const prevTokens = this.props.balance.tokens.map((t) => `${t.token.tag}_${t.value.toNumber()}`);
|
||||
const nextTokens = nextProps.balance.tokens.map((t) => `${t.token.tag}_${t.value.toNumber()}`);
|
||||
|
||||
if (!isEqual(prevTokens, nextTokens)) {
|
||||
this.computeTokens(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
computeTokens (props = this.props) {
|
||||
const { api } = this.context;
|
||||
const { balance } = this.props;
|
||||
|
||||
const items = balance.tokens
|
||||
.filter((token, index) => !index || token.value.gt(0))
|
||||
.map((balance, index) => {
|
||||
const token = balance.token;
|
||||
const isEth = index === 0;
|
||||
|
||||
let value = 0;
|
||||
|
||||
if (isEth) {
|
||||
value = api.util.fromWei(balance.value).toFormat(3);
|
||||
} else {
|
||||
const format = balance.token.format || 1;
|
||||
const decimals = format === 1 ? 0 : Math.min(3, Math.floor(format / 10));
|
||||
|
||||
value = new BigNumber(balance.value).div(format).toFormat(decimals);
|
||||
}
|
||||
|
||||
const label = (
|
||||
<div className={ styles.token }>
|
||||
<TokenImage token={ token } />
|
||||
<div className={ styles.tokenname }>
|
||||
{ token.name }
|
||||
</div>
|
||||
<div className={ styles.tokenbalance }>
|
||||
{ value }<small> { token.tag }</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={ `${index}_${token.tag}` }
|
||||
value={ token.tag }
|
||||
label={ label }
|
||||
>
|
||||
{ label }
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
this.setState({ items });
|
||||
}
|
||||
|
||||
render () {
|
||||
const { tag, onChange } = this.props;
|
||||
const { items } = this.state;
|
||||
|
||||
return (
|
||||
<Select
|
||||
className={ styles.tokenSelect }
|
||||
label='type of token transfer'
|
||||
hint='type of token to transfer'
|
||||
value={ tag }
|
||||
onChange={ onChange }
|
||||
>
|
||||
{ items }
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
}
|
@ -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 }
|
||||
|
23
js/src/redux/providers/workerWrapper.js
Normal file
23
js/src/redux/providers/workerWrapper.js
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
if (!process.env.EMBED) {
|
||||
const setupWorker = require('./worker').setupWorker;
|
||||
|
||||
module.exports = { setupWorker };
|
||||
} else {
|
||||
module.exports = { setupWorker: () => {} };
|
||||
}
|
@ -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,
|
||||
|
@ -65,7 +65,7 @@ describe('ui/AccountCard', () => {
|
||||
let balance;
|
||||
|
||||
beforeEach(() => {
|
||||
balance = component.find('Connect(Balance)');
|
||||
balance = component.find('Balance');
|
||||
});
|
||||
|
||||
it('renders the balance', () => {
|
||||
|
@ -14,4 +14,9 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
export Export from './Export';
|
||||
export Import from './Import';
|
||||
export Search from './Search';
|
||||
export Sort from './Sort';
|
||||
|
||||
export default from './actionbar';
|
||||
|
@ -17,13 +17,12 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import unknownImage from '~/../assets/images/contracts/unknown-64x64.png';
|
||||
import TokenImage from '~/ui/TokenImage';
|
||||
|
||||
import styles from './balance.css';
|
||||
|
||||
class Balance extends Component {
|
||||
export default class Balance extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object
|
||||
};
|
||||
@ -31,7 +30,6 @@ class Balance extends Component {
|
||||
static propTypes = {
|
||||
balance: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
images: PropTypes.object.isRequired,
|
||||
showOnlyEth: PropTypes.bool,
|
||||
showZeroValues: PropTypes.bool
|
||||
};
|
||||
@ -43,7 +41,7 @@ class Balance extends Component {
|
||||
|
||||
render () {
|
||||
const { api } = this.context;
|
||||
const { balance, className, images, showZeroValues, showOnlyEth } = this.props;
|
||||
const { balance, className, showZeroValues, showOnlyEth } = this.props;
|
||||
|
||||
if (!balance || !balance.tokens) {
|
||||
return null;
|
||||
@ -79,26 +77,12 @@ class Balance extends Component {
|
||||
value = api.util.fromWei(balance.value).toFormat(3);
|
||||
}
|
||||
|
||||
const imageurl = token.image || images[token.address];
|
||||
let imagesrc = unknownImage;
|
||||
|
||||
if (imageurl) {
|
||||
const host = /^(\/)?api/.test(imageurl)
|
||||
? api.dappsUrl
|
||||
: '';
|
||||
|
||||
imagesrc = `${host}${imageurl}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ styles.balance }
|
||||
key={ `${index}_${token.tag}` }
|
||||
>
|
||||
<img
|
||||
src={ imagesrc }
|
||||
alt={ token.name }
|
||||
/>
|
||||
<TokenImage token={ token } />
|
||||
<div className={ styles.balanceValue }>
|
||||
<span title={ value }> { value } </span>
|
||||
</div>
|
||||
@ -125,14 +109,3 @@ class Balance extends Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const { images } = state;
|
||||
|
||||
return { images };
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(Balance);
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import apiutil from '~/api/util';
|
||||
|
||||
@ -32,7 +31,6 @@ const BALANCE = {
|
||||
|
||||
let api;
|
||||
let component;
|
||||
let store;
|
||||
|
||||
function createApi () {
|
||||
api = {
|
||||
@ -43,36 +41,22 @@ function createApi () {
|
||||
return api;
|
||||
}
|
||||
|
||||
function createStore () {
|
||||
store = {
|
||||
dispatch: sinon.stub(),
|
||||
subscribe: sinon.stub(),
|
||||
getState: () => {
|
||||
return {
|
||||
images: {}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
function render (props = {}) {
|
||||
if (!props.balance) {
|
||||
props.balance = BALANCE;
|
||||
}
|
||||
|
||||
const api = createApi();
|
||||
|
||||
component = shallow(
|
||||
<Balance
|
||||
className='testClass'
|
||||
{ ...props }
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
store: createStore()
|
||||
}
|
||||
context: { api }
|
||||
}
|
||||
).find('Balance').shallow({ context: { api: createApi() } });
|
||||
);
|
||||
|
||||
return component;
|
||||
}
|
||||
@ -91,18 +75,18 @@ describe('ui/Balance', () => {
|
||||
});
|
||||
|
||||
it('renders all the non-zero balances', () => {
|
||||
expect(component.find('img')).to.have.length(2);
|
||||
expect(component.find('Connect(TokenImage)')).to.have.length(2);
|
||||
});
|
||||
|
||||
describe('render specifiers', () => {
|
||||
it('renders only the single token with showOnlyEth', () => {
|
||||
render({ showOnlyEth: true });
|
||||
expect(component.find('img')).to.have.length(1);
|
||||
expect(component.find('Connect(TokenImage)')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('renders all the tokens with showZeroValues', () => {
|
||||
render({ showZeroValues: true });
|
||||
expect(component.find('img')).to.have.length(3);
|
||||
expect(component.find('Connect(TokenImage)')).to.have.length(3);
|
||||
});
|
||||
|
||||
it('shows ETH with zero value with showOnlyEth & showZeroValues', () => {
|
||||
@ -116,7 +100,7 @@ describe('ui/Balance', () => {
|
||||
]
|
||||
}
|
||||
});
|
||||
expect(component.find('img')).to.have.length(1);
|
||||
expect(component.find('Connect(TokenImage)')).to.have.length(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -14,35 +14,19 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import AddressSelect from './AddressSelect';
|
||||
import DappUrlInput from './DappUrlInput';
|
||||
import FormWrap from './FormWrap';
|
||||
import Input from './Input';
|
||||
import InputAddress from './InputAddress';
|
||||
import InputAddressSelect from './InputAddressSelect';
|
||||
import InputChip from './InputChip';
|
||||
import InputDate from './InputDate';
|
||||
import InputInline from './InputInline';
|
||||
import InputTime from './InputTime';
|
||||
import Label from './Label';
|
||||
import RadioButtons from './RadioButtons';
|
||||
import Select from './Select';
|
||||
import TypedInput from './TypedInput';
|
||||
export AddressSelect from './AddressSelect';
|
||||
export DappUrlInput from './DappUrlInput';
|
||||
export FormWrap from './FormWrap';
|
||||
export Input from './Input';
|
||||
export InputAddress from './InputAddress';
|
||||
export InputAddressSelect from './InputAddressSelect';
|
||||
export InputChip from './InputChip';
|
||||
export InputDate from './InputDate';
|
||||
export InputInline from './InputInline';
|
||||
export InputTime from './InputTime';
|
||||
export Label from './Label';
|
||||
export RadioButtons from './RadioButtons';
|
||||
export Select from './Select';
|
||||
export TypedInput from './TypedInput';
|
||||
|
||||
export default from './form';
|
||||
export {
|
||||
AddressSelect,
|
||||
DappUrlInput,
|
||||
FormWrap,
|
||||
Input,
|
||||
InputAddress,
|
||||
InputAddressSelect,
|
||||
InputChip,
|
||||
InputDate,
|
||||
InputInline,
|
||||
InputTime,
|
||||
Label,
|
||||
RadioButtons,
|
||||
Select,
|
||||
TypedInput
|
||||
};
|
||||
|
17
js/src/ui/TokenImage/index.js
Normal file
17
js/src/ui/TokenImage/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
export default from './tokenImage';
|
72
js/src/ui/TokenImage/tokenImage.js
Normal file
72
js/src/ui/TokenImage/tokenImage.js
Normal file
@ -0,0 +1,72 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import unknownImage from '~/../assets/images/contracts/unknown-64x64.png';
|
||||
|
||||
class TokenImage extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
image: PropTypes.string,
|
||||
token: PropTypes.shape({
|
||||
image: PropTypes.string,
|
||||
address: PropTypes.string
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
render () {
|
||||
const { api } = this.context;
|
||||
const { image, token } = this.props;
|
||||
|
||||
const imageurl = token.image || image;
|
||||
let imagesrc = unknownImage;
|
||||
|
||||
if (imageurl) {
|
||||
const host = /^(\/)?api/.test(imageurl)
|
||||
? api.dappsUrl
|
||||
: '';
|
||||
|
||||
imagesrc = `${host}${imageurl}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={ imagesrc }
|
||||
alt={ token.name }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (iniState) {
|
||||
const { images } = iniState;
|
||||
|
||||
return (_, props) => {
|
||||
const { token } = props;
|
||||
|
||||
return { image: images[token.address] };
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(TokenImage);
|
@ -16,8 +16,10 @@
|
||||
|
||||
import moment from 'moment';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { txLink, addressLink } from '~/3rdparty/etherscan/links';
|
||||
import { txLink } from '~/3rdparty/etherscan/links';
|
||||
|
||||
import IdentityIcon from '../../IdentityIcon';
|
||||
import IdentityName from '../../IdentityName';
|
||||
@ -25,19 +27,20 @@ import MethodDecoding from '../../MethodDecoding';
|
||||
|
||||
import styles from '../txList.css';
|
||||
|
||||
export default class TxRow extends Component {
|
||||
class TxRow extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
tx: PropTypes.object.isRequired,
|
||||
accountAddresses: PropTypes.array.isRequired,
|
||||
address: PropTypes.string.isRequired,
|
||||
isTest: PropTypes.bool.isRequired,
|
||||
tx: PropTypes.object.isRequired,
|
||||
|
||||
block: PropTypes.object,
|
||||
historic: PropTypes.bool,
|
||||
className: PropTypes.string
|
||||
className: PropTypes.string,
|
||||
historic: PropTypes.bool
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -77,22 +80,20 @@ export default class TxRow extends Component {
|
||||
}
|
||||
|
||||
renderAddress (address) {
|
||||
const { isTest } = this.props;
|
||||
|
||||
let esLink = null;
|
||||
|
||||
if (address) {
|
||||
esLink = (
|
||||
<a
|
||||
href={ addressLink(address, isTest) }
|
||||
target='_blank'
|
||||
<Link
|
||||
activeClassName={ styles.currentLink }
|
||||
className={ styles.link }
|
||||
to={ this.addressLink(address) }
|
||||
>
|
||||
<IdentityName
|
||||
address={ address }
|
||||
shorten
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@ -138,4 +139,30 @@ export default class TxRow extends Component {
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
addressLink (address) {
|
||||
const { accountAddresses } = this.props;
|
||||
const isAccount = accountAddresses.includes(address);
|
||||
|
||||
if (isAccount) {
|
||||
return `/accounts/${address}`;
|
||||
}
|
||||
|
||||
return `/addresses/${address}`;
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (initState) {
|
||||
const { accounts } = initState.personal;
|
||||
const accountAddresses = Object.keys(accounts);
|
||||
|
||||
return () => {
|
||||
return { accountAddresses };
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(TxRow);
|
||||
|
||||
|
@ -25,13 +25,28 @@ import TxRow from './txRow';
|
||||
|
||||
const api = new Api({ execute: sinon.stub() });
|
||||
|
||||
const STORE = {
|
||||
dispatch: sinon.stub(),
|
||||
subscribe: sinon.stub(),
|
||||
getState: () => {
|
||||
return {
|
||||
personal: {
|
||||
accounts: {
|
||||
'0x123': {}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function render (props) {
|
||||
return shallow(
|
||||
<TxRow
|
||||
store={ STORE }
|
||||
{ ...props }
|
||||
/>,
|
||||
{ context: { api } }
|
||||
);
|
||||
).find('TxRow').shallow({ context: { api } });
|
||||
}
|
||||
|
||||
describe('ui/TxList/TxRow', () => {
|
||||
@ -48,5 +63,37 @@ describe('ui/TxList/TxRow', () => {
|
||||
|
||||
expect(render({ address: '0x123', block, isTest: true, tx })).to.be.ok;
|
||||
});
|
||||
|
||||
it('renders an account link', () => {
|
||||
const block = {
|
||||
timestamp: new Date()
|
||||
};
|
||||
const tx = {
|
||||
blockNumber: new BigNumber(123),
|
||||
hash: '0x123456789abcdef0123456789abcdef0123456789abcdef',
|
||||
to: '0x123',
|
||||
value: new BigNumber(1)
|
||||
};
|
||||
|
||||
const element = render({ address: '0x123', block, isTest: true, tx });
|
||||
|
||||
expect(element.find('Link').prop('to')).to.equal('/accounts/0x123');
|
||||
});
|
||||
|
||||
it('renders an address link', () => {
|
||||
const block = {
|
||||
timestamp: new Date()
|
||||
};
|
||||
const tx = {
|
||||
blockNumber: new BigNumber(123),
|
||||
hash: '0x123456789abcdef0123456789abcdef0123456789abcdef',
|
||||
to: '0x456',
|
||||
value: new BigNumber(1)
|
||||
};
|
||||
|
||||
const element = render({ address: '0x123', block, isTest: true, tx });
|
||||
|
||||
expect(element.find('Link').prop('to')).to.equal('/addresses/0x456');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -65,6 +65,11 @@
|
||||
|
||||
.link {
|
||||
vertical-align: top;
|
||||
|
||||
&.currentLink {
|
||||
color: white;
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
|
@ -14,118 +14,43 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import AccountCard from './AccountCard';
|
||||
import Actionbar from './Actionbar';
|
||||
import ActionbarExport from './Actionbar/Export';
|
||||
import ActionbarImport from './Actionbar/Import';
|
||||
import ActionbarSearch from './Actionbar/Search';
|
||||
import ActionbarSort from './Actionbar/Sort';
|
||||
import Badge from './Badge';
|
||||
import Balance from './Balance';
|
||||
import BlockStatus from './BlockStatus';
|
||||
import Button from './Button';
|
||||
import Certifications from './Certifications';
|
||||
import ConfirmDialog from './ConfirmDialog';
|
||||
import Container, { Title as ContainerTitle } from './Container';
|
||||
import ContextProvider from './ContextProvider';
|
||||
import CopyToClipboard from './CopyToClipboard';
|
||||
import CurrencySymbol from './CurrencySymbol';
|
||||
import DappCard from './DappCard';
|
||||
import DappIcon from './DappIcon';
|
||||
import Editor from './Editor';
|
||||
import Errors from './Errors';
|
||||
import Features, { FEATURES, FeaturesStore } from './Features';
|
||||
import Form, { AddressSelect, DappUrlInput, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form';
|
||||
import GasPriceEditor from './GasPriceEditor';
|
||||
import GasPriceSelector from './GasPriceSelector';
|
||||
import Icons from './Icons';
|
||||
import IdentityIcon from './IdentityIcon';
|
||||
import IdentityName from './IdentityName';
|
||||
import LanguageSelector from './LanguageSelector';
|
||||
import Loading from './Loading';
|
||||
import MethodDecoding from './MethodDecoding';
|
||||
import Modal, { Busy as BusyStep, Completed as CompletedStep } from './Modal';
|
||||
import muiTheme from './Theme';
|
||||
import Page from './Page';
|
||||
import ParityBackground from './ParityBackground';
|
||||
import PasswordStrength from './Form/PasswordStrength';
|
||||
import Portal from './Portal';
|
||||
import QrCode from './QrCode';
|
||||
import SectionList from './SectionList';
|
||||
import ShortenedHash from './ShortenedHash';
|
||||
import SignerIcon from './SignerIcon';
|
||||
import Tags from './Tags';
|
||||
import Title from './Title';
|
||||
import Tooltips, { Tooltip } from './Tooltips';
|
||||
import TxHash from './TxHash';
|
||||
import TxList from './TxList';
|
||||
import Warning from './Warning';
|
||||
|
||||
export {
|
||||
AccountCard,
|
||||
Actionbar,
|
||||
ActionbarExport,
|
||||
ActionbarImport,
|
||||
ActionbarSearch,
|
||||
ActionbarSort,
|
||||
AddressSelect,
|
||||
Badge,
|
||||
Balance,
|
||||
BlockStatus,
|
||||
Button,
|
||||
Certifications,
|
||||
ConfirmDialog,
|
||||
Container,
|
||||
ContainerTitle,
|
||||
ContextProvider,
|
||||
CopyToClipboard,
|
||||
CurrencySymbol,
|
||||
DappCard,
|
||||
DappIcon,
|
||||
DappUrlInput,
|
||||
Editor,
|
||||
Errors,
|
||||
FEATURES,
|
||||
Features,
|
||||
FeaturesStore,
|
||||
Form,
|
||||
FormWrap,
|
||||
GasPriceEditor,
|
||||
GasPriceSelector,
|
||||
Icons,
|
||||
Input,
|
||||
InputAddress,
|
||||
InputAddressSelect,
|
||||
InputChip,
|
||||
InputDate,
|
||||
InputInline,
|
||||
InputTime,
|
||||
IdentityIcon,
|
||||
IdentityName,
|
||||
Label,
|
||||
LanguageSelector,
|
||||
Loading,
|
||||
MethodDecoding,
|
||||
Modal,
|
||||
BusyStep,
|
||||
CompletedStep,
|
||||
muiTheme,
|
||||
Page,
|
||||
ParityBackground,
|
||||
PasswordStrength,
|
||||
Portal,
|
||||
QrCode,
|
||||
RadioButtons,
|
||||
Select,
|
||||
ShortenedHash,
|
||||
SectionList,
|
||||
SignerIcon,
|
||||
Tags,
|
||||
Title,
|
||||
Tooltip,
|
||||
Tooltips,
|
||||
TxHash,
|
||||
TxList,
|
||||
TypedInput,
|
||||
Warning
|
||||
};
|
||||
export AccountCard from './AccountCard';
|
||||
export Actionbar, { Export as ActionbarExport, Import as ActionbarImport, Search as ActionbarSearch, Sort as ActionbarSort } from './Actionbar';
|
||||
export Badge from './Badge';
|
||||
export Balance from './Balance';
|
||||
export BlockStatus from './BlockStatus';
|
||||
export Button from './Button';
|
||||
export Certifications from './Certifications';
|
||||
export ConfirmDialog from './ConfirmDialog';
|
||||
export Container, { Title as ContainerTitle } from './Container';
|
||||
export ContextProvider from './ContextProvider';
|
||||
export CopyToClipboard from './CopyToClipboard';
|
||||
export CurrencySymbol from './CurrencySymbol';
|
||||
export DappCard from './DappCard';
|
||||
export DappIcon from './DappIcon';
|
||||
export Errors from './Errors';
|
||||
export Features, { FEATURES, FeaturesStore } from './Features';
|
||||
export Form, { AddressSelect, DappUrlInput, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form';
|
||||
export GasPriceEditor from './GasPriceEditor';
|
||||
export GasPriceSelector from './GasPriceSelector';
|
||||
export Icons from './Icons';
|
||||
export IdentityIcon from './IdentityIcon';
|
||||
export IdentityName from './IdentityName';
|
||||
export LanguageSelector from './LanguageSelector';
|
||||
export Loading from './Loading';
|
||||
export MethodDecoding from './MethodDecoding';
|
||||
export Modal, { Busy as BusyStep, Completed as CompletedStep } from './Modal';
|
||||
export muiTheme from './Theme';
|
||||
export Page from './Page';
|
||||
export ParityBackground from './ParityBackground';
|
||||
export Portal from './Portal';
|
||||
export QrCode from './QrCode';
|
||||
export SectionList from './SectionList';
|
||||
export ShortenedHash from './ShortenedHash';
|
||||
export SignerIcon from './SignerIcon';
|
||||
export Tags from './Tags';
|
||||
export Title from './Title';
|
||||
export Tooltips, { Tooltip } from './Tooltips';
|
||||
export TxHash from './TxHash';
|
||||
export TxList from './TxList';
|
||||
export Warning from './Warning';
|
||||
|
@ -15,7 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import scrypt from 'scryptsy';
|
||||
import * as Transaction from 'ethereumjs-tx';
|
||||
import Transaction from 'ethereumjs-tx';
|
||||
import { pbkdf2Sync } from 'crypto';
|
||||
import { createDecipheriv } from 'browserify-aes';
|
||||
|
||||
|
@ -73,7 +73,7 @@ describe('views/Account/Header', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
render({ balance: { balance: 'testing' } });
|
||||
balance = component.find('Connect(Balance)');
|
||||
balance = component.find('Balance');
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
|
@ -37,7 +37,6 @@ class Account extends Component {
|
||||
static propTypes = {
|
||||
fetchCertifiers: PropTypes.func.isRequired,
|
||||
fetchCertifications: PropTypes.func.isRequired,
|
||||
images: PropTypes.object.isRequired,
|
||||
setVisibleAccounts: PropTypes.func.isRequired,
|
||||
|
||||
accounts: PropTypes.object,
|
||||
@ -257,14 +256,13 @@ class Account extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { balances, images } = this.props;
|
||||
const { balances } = this.props;
|
||||
|
||||
return (
|
||||
<Transfer
|
||||
account={ account }
|
||||
balance={ balance }
|
||||
balances={ balances }
|
||||
images={ images }
|
||||
onClose={ this.store.toggleTransferDialog }
|
||||
/>
|
||||
);
|
||||
@ -289,12 +287,10 @@ class Account extends Component {
|
||||
function mapStateToProps (state) {
|
||||
const { accounts } = state.personal;
|
||||
const { balances } = state.balances;
|
||||
const { images } = state;
|
||||
|
||||
return {
|
||||
accounts,
|
||||
balances,
|
||||
images
|
||||
balances
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
|
@ -285,6 +285,7 @@ class ParityBar extends Component {
|
||||
}
|
||||
|
||||
renderExpanded () {
|
||||
const { externalLink } = this.props;
|
||||
const { displayType } = this.state;
|
||||
|
||||
return (
|
||||
@ -333,7 +334,7 @@ class ParityBar extends Component {
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Signer />
|
||||
<Signer externalLink={ externalLink } />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
@ -15,16 +15,19 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { addressLink } from '~/3rdparty/etherscan/links';
|
||||
import styles from './accountLink.css';
|
||||
|
||||
export default class AccountLink extends Component {
|
||||
class AccountLink extends Component {
|
||||
static propTypes = {
|
||||
isTest: PropTypes.bool.isRequired,
|
||||
accountAddresses: PropTypes.array.isRequired,
|
||||
address: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node
|
||||
children: PropTypes.node,
|
||||
externalLink: PropTypes.string.isRequired,
|
||||
isTest: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
state = {
|
||||
@ -32,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 (
|
||||
<a
|
||||
href={ this.state.link }
|
||||
target='_blank'
|
||||
className={ `${styles.container} ${className}` }
|
||||
>
|
||||
{ children || address }
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={ this.state.link }
|
||||
target='_blank'
|
||||
<Link
|
||||
className={ `${styles.container} ${className}` }
|
||||
to={ this.state.link }
|
||||
>
|
||||
{ children || address }
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -25,6 +25,7 @@ export default class Account extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
address: PropTypes.string.isRequired,
|
||||
externalLink: PropTypes.string.isRequired,
|
||||
isTest: PropTypes.bool.isRequired,
|
||||
balance: PropTypes.object // eth BigNumber, not required since it mght take time to fetch
|
||||
};
|
||||
@ -51,12 +52,13 @@ export default class Account extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { address, isTest, className } = this.props;
|
||||
const { address, externalLink, isTest, className } = this.props;
|
||||
|
||||
return (
|
||||
<div className={ `${styles.acc} ${className}` }>
|
||||
<AccountLink
|
||||
address={ address }
|
||||
externalLink={ externalLink }
|
||||
isTest={ isTest }
|
||||
>
|
||||
<IdentityIcon
|
||||
@ -79,13 +81,14 @@ export default class Account extends Component {
|
||||
}
|
||||
|
||||
renderName () {
|
||||
const { address, isTest } = this.props;
|
||||
const { address, externalLink, isTest } = this.props;
|
||||
const name = <IdentityName address={ address } empty />;
|
||||
|
||||
if (!name) {
|
||||
return (
|
||||
<AccountLink
|
||||
address={ address }
|
||||
externalLink={ externalLink }
|
||||
isTest={ isTest }
|
||||
>
|
||||
[{ this.shortAddress(address) }]
|
||||
@ -96,6 +99,7 @@ export default class Account extends Component {
|
||||
return (
|
||||
<AccountLink
|
||||
address={ address }
|
||||
externalLink={ externalLink }
|
||||
isTest={ isTest }
|
||||
>
|
||||
<span>
|
||||
|
@ -93,7 +93,9 @@ export default class SignRequest extends Component {
|
||||
renderDetails () {
|
||||
const { api } = this.context;
|
||||
const { address, isTest, store, data } = this.props;
|
||||
const balance = store.balances[address];
|
||||
const { balances, externalLink } = store;
|
||||
|
||||
const balance = balances[address];
|
||||
|
||||
if (!balance) {
|
||||
return <div />;
|
||||
@ -105,6 +107,7 @@ export default class SignRequest extends Component {
|
||||
<Account
|
||||
address={ address }
|
||||
balance={ balance }
|
||||
externalLink={ externalLink }
|
||||
isTest={ isTest }
|
||||
/>
|
||||
</div>
|
||||
|
@ -27,6 +27,7 @@ import styles from './transactionMainDetails.css';
|
||||
export default class TransactionMainDetails extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
externalLink: PropTypes.string.isRequired,
|
||||
from: PropTypes.string.isRequired,
|
||||
fromBalance: PropTypes.object,
|
||||
gasStore: PropTypes.object,
|
||||
@ -50,7 +51,7 @@ export default class TransactionMainDetails extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, from, fromBalance, gasStore, isTest, transaction } = this.props;
|
||||
const { children, externalLink, from, fromBalance, gasStore, isTest, transaction } = this.props;
|
||||
|
||||
return (
|
||||
<div className={ styles.transaction }>
|
||||
@ -59,6 +60,7 @@ export default class TransactionMainDetails extends Component {
|
||||
<Account
|
||||
address={ from }
|
||||
balance={ fromBalance }
|
||||
externalLink={ externalLink }
|
||||
isTest={ isTest }
|
||||
/>
|
||||
</div>
|
||||
|
@ -89,14 +89,16 @@ export default class TransactionPending extends Component {
|
||||
renderTransaction () {
|
||||
const { className, focus, id, isSending, isTest, store, transaction } = this.props;
|
||||
const { totalValue } = this.state;
|
||||
const { balances, externalLink } = store;
|
||||
const { from, value } = transaction;
|
||||
|
||||
const fromBalance = store.balances[from];
|
||||
const fromBalance = balances[from];
|
||||
|
||||
return (
|
||||
<div className={ `${styles.container} ${className}` }>
|
||||
<TransactionMainDetails
|
||||
className={ styles.transactionDetails }
|
||||
externalLink={ externalLink }
|
||||
from={ from }
|
||||
fromBalance={ fromBalance }
|
||||
gasStore={ this.gasStore }
|
||||
|
@ -37,6 +37,7 @@ class Embedded extends Component {
|
||||
startConfirmRequest: PropTypes.func.isRequired,
|
||||
startRejectRequest: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
externalLink: PropTypes.string,
|
||||
gasLimit: PropTypes.object.isRequired,
|
||||
isTest: PropTypes.bool.isRequired,
|
||||
signer: PropTypes.shape({
|
||||
@ -45,7 +46,7 @@ class Embedded extends Component {
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
store = new Store(this.context.api);
|
||||
store = new Store(this.context.api, false, this.props.externalLink);
|
||||
|
||||
render () {
|
||||
return (
|
||||
|
@ -17,13 +17,16 @@
|
||||
import { isEqual } from 'lodash';
|
||||
import { action, observable } from 'mobx';
|
||||
|
||||
export default class Store {
|
||||
export default class SignerStore {
|
||||
@observable balances = {};
|
||||
@observable localHashes = [];
|
||||
|
||||
constructor (api, withLocalTransactions = false) {
|
||||
externalLink = '';
|
||||
|
||||
constructor (api, withLocalTransactions = false, externalLink = '') {
|
||||
this._api = api;
|
||||
this._timeoutId = 0;
|
||||
this.externalLink = externalLink;
|
||||
|
||||
if (withLocalTransactions) {
|
||||
this.fetchLocalTransactions();
|
||||
|
@ -66,7 +66,6 @@ class Wallet extends Component {
|
||||
static propTypes = {
|
||||
address: PropTypes.string.isRequired,
|
||||
balance: nullableProptype(PropTypes.object.isRequired),
|
||||
images: PropTypes.object.isRequired,
|
||||
isTest: PropTypes.bool.isRequired,
|
||||
owned: PropTypes.bool.isRequired,
|
||||
setVisibleAccounts: PropTypes.func.isRequired,
|
||||
@ -315,13 +314,12 @@ class Wallet extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { walletAccount, balance, images } = this.props;
|
||||
const { walletAccount, balance } = this.props;
|
||||
|
||||
return (
|
||||
<Transfer
|
||||
account={ walletAccount }
|
||||
balance={ balance }
|
||||
images={ images }
|
||||
onClose={ this.onTransferClose }
|
||||
/>
|
||||
);
|
||||
@ -365,7 +363,6 @@ function mapStateToProps (_, initProps) {
|
||||
const { isTest } = state.nodeStatus;
|
||||
const { accountsInfo = {}, accounts = {} } = state.personal;
|
||||
const { balances } = state.balances;
|
||||
const { images } = state;
|
||||
const walletAccount = accounts[address] || accountsInfo[address] || null;
|
||||
|
||||
if (walletAccount) {
|
||||
@ -379,7 +376,6 @@ function mapStateToProps (_, initProps) {
|
||||
return {
|
||||
address,
|
||||
balance,
|
||||
images,
|
||||
isTest,
|
||||
owned,
|
||||
wallet,
|
||||
|
@ -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';
|
||||
|
@ -14,44 +14,20 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import Account from './Account';
|
||||
import Accounts from './Accounts';
|
||||
import Address from './Address';
|
||||
import Addresses from './Addresses';
|
||||
import Application from './Application';
|
||||
import Contract from './Contract';
|
||||
import Contracts from './Contracts';
|
||||
import Dapp from './Dapp';
|
||||
import Dapps from './Dapps';
|
||||
import HistoryStore from './historyStore';
|
||||
import ParityBar from './ParityBar';
|
||||
import Settings, { SettingsBackground, SettingsParity, SettingsProxy, SettingsViews } from './Settings';
|
||||
import Signer from './Signer';
|
||||
import Status from './Status';
|
||||
import Wallet from './Wallet';
|
||||
import Web from './Web';
|
||||
import WriteContract from './WriteContract';
|
||||
|
||||
export {
|
||||
Account,
|
||||
Accounts,
|
||||
Address,
|
||||
Addresses,
|
||||
Application,
|
||||
Contract,
|
||||
Contracts,
|
||||
Dapp,
|
||||
Dapps,
|
||||
HistoryStore,
|
||||
ParityBar,
|
||||
Settings,
|
||||
SettingsBackground,
|
||||
SettingsParity,
|
||||
SettingsProxy,
|
||||
SettingsViews,
|
||||
Signer,
|
||||
Status,
|
||||
Wallet,
|
||||
Web,
|
||||
WriteContract
|
||||
};
|
||||
export Account from './Account';
|
||||
export Accounts from './Accounts';
|
||||
export Address from './Address';
|
||||
export Addresses from './Addresses';
|
||||
export Application from './Application';
|
||||
export Contract from './Contract';
|
||||
export Contracts from './Contracts';
|
||||
export Dapp from './Dapp';
|
||||
export Dapps from './Dapps';
|
||||
export HistoryStore from './historyStore';
|
||||
export ParityBar from './ParityBar';
|
||||
export Settings, { SettingsBackground, SettingsParity, SettingsProxy, SettingsViews } from './Settings';
|
||||
export Signer from './Signer';
|
||||
export Status from './Status';
|
||||
export Wallet from './Wallet';
|
||||
export Web from './Web';
|
||||
export WriteContract from './WriteContract';
|
||||
|
@ -15,7 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try {
|
||||
var JsonRpc = require('../.npmjs/jsonRpc/library.js');
|
||||
var JsonRpc = require('../.npmjs/jsonrpc/library.js').default;
|
||||
|
||||
if (typeof JsonRpc !== 'object') {
|
||||
throw new Error('JsonRpc');
|
||||
|
@ -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');
|
||||
|
||||
|
@ -36,4 +36,5 @@ app.use(wsProxy);
|
||||
var server = app.listen(process.env.PORT || 3000, function () {
|
||||
console.log('Listening on port', server.address().port);
|
||||
});
|
||||
|
||||
server.on('upgrade', wsProxy.upgrade);
|
||||
|
49
js/webpack/embed.js
Normal file
49
js/webpack/embed.js
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
const webpack = require('webpack');
|
||||
const WebpackStats = require('webpack/lib/Stats');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const WebpackConfig = require('./app');
|
||||
|
||||
const compiler = webpack(WebpackConfig);
|
||||
|
||||
compiler.run(function handler (err, stats) {
|
||||
if (err) {
|
||||
return console.error(err);
|
||||
}
|
||||
|
||||
// @see https://github.com/webpack/webpack/blob/324d309107f00cfc38ec727521563d309339b2ec/lib/Stats.js#L790
|
||||
// Accepted values: none, errors-only, minimal, normal, verbose
|
||||
const options = WebpackStats.presetToOptions('normal');
|
||||
|
||||
options.timings = true;
|
||||
|
||||
const output = stats.toString(options);
|
||||
const assets = Object.keys(stats.compilation.assets);
|
||||
|
||||
const embedPath = path.resolve(WebpackConfig.output.path, './embed.json');
|
||||
const embedData = { assets: assets };
|
||||
|
||||
fs.writeFileSync(embedPath, JSON.stringify(embedData, null, 2));
|
||||
|
||||
process.stdout.write('\n');
|
||||
process.stdout.write(output);
|
||||
process.stdout.write('\n\n');
|
||||
});
|
||||
|
@ -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),
|
||||
|
@ -37,7 +37,7 @@
|
||||
0ACF9AC61E30FAB600D5C935 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
0ACF9AC81E30FAB600D5C935 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
0AE564F01E3CE42C00BD01F7 /* GetBSDProcessList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetBSDProcessList.swift; sourceTree = "<group>"; };
|
||||
0AED4D9F1E3E22F800BF87C0 /* ethstore */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = ethstore; path = ../target/release/deps/ethstore; sourceTree = "<group>"; };
|
||||
0AED4D9F1E3E22F800BF87C0 /* ethstore */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = ethstore; path = ../target/release/ethstore; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -18,7 +18,7 @@ use std::path::PathBuf;
|
||||
use ethcore::ethstore::{EthStore, SecretStore, import_accounts, read_geth_accounts};
|
||||
use ethcore::ethstore::dir::RootDiskDirectory;
|
||||
use ethcore::ethstore::SecretVaultRef;
|
||||
use ethcore::account_provider::AccountProvider;
|
||||
use ethcore::account_provider::{AccountProvider, AccountProviderSettings};
|
||||
use helpers::{password_prompt, password_from_file};
|
||||
use params::SpecType;
|
||||
|
||||
@ -92,7 +92,7 @@ fn new(n: NewAccount) -> Result<String, String> {
|
||||
|
||||
let dir = Box::new(keys_dir(n.path, n.spec)?);
|
||||
let secret_store = Box::new(secret_store(dir, Some(n.iterations))?);
|
||||
let acc_provider = AccountProvider::new(secret_store);
|
||||
let acc_provider = AccountProvider::new(secret_store, AccountProviderSettings::default());
|
||||
let new_account = acc_provider.new_account(&password).map_err(|e| format!("Could not create new account: {}", e))?;
|
||||
Ok(format!("{:?}", new_account))
|
||||
}
|
||||
@ -100,7 +100,7 @@ fn new(n: NewAccount) -> Result<String, String> {
|
||||
fn list(list_cmd: ListAccounts) -> Result<String, String> {
|
||||
let dir = Box::new(keys_dir(list_cmd.path, list_cmd.spec)?);
|
||||
let secret_store = Box::new(secret_store(dir, None)?);
|
||||
let acc_provider = AccountProvider::new(secret_store);
|
||||
let acc_provider = AccountProvider::new(secret_store, AccountProviderSettings::default());
|
||||
let accounts = acc_provider.accounts();
|
||||
let result = accounts.into_iter()
|
||||
.map(|a| format!("{:?}", a))
|
||||
|
@ -101,6 +101,9 @@ usage! {
|
||||
or |c: &Config| otry!(c.account).password.clone(),
|
||||
flag_keys_iterations: u32 = 10240u32,
|
||||
or |c: &Config| otry!(c.account).keys_iterations.clone(),
|
||||
flag_no_hardware_wallets: bool = false,
|
||||
or |c: &Config| otry!(c.account).disable_hardware.clone(),
|
||||
|
||||
|
||||
flag_force_ui: bool = false,
|
||||
or |c: &Config| otry!(c.ui).force.clone(),
|
||||
@ -347,6 +350,7 @@ struct Account {
|
||||
unlock: Option<Vec<String>>,
|
||||
password: Option<Vec<String>>,
|
||||
keys_iterations: Option<u32>,
|
||||
disable_hardware: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, RustcDecodable)]
|
||||
@ -583,6 +587,7 @@ mod tests {
|
||||
flag_unlock: Some("0xdeadbeefcafe0000000000000000000000000000".into()),
|
||||
flag_password: vec!["~/.safe/password.file".into()],
|
||||
flag_keys_iterations: 10240u32,
|
||||
flag_no_hardware_wallets: false,
|
||||
|
||||
flag_force_ui: false,
|
||||
flag_no_ui: false,
|
||||
@ -769,6 +774,7 @@ mod tests {
|
||||
unlock: Some(vec!["0x1".into(), "0x2".into(), "0x3".into()]),
|
||||
password: Some(vec!["passwdfile path".into()]),
|
||||
keys_iterations: None,
|
||||
disable_hardware: None,
|
||||
}),
|
||||
ui: Some(Ui {
|
||||
force: None,
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -175,6 +175,7 @@ pub struct AccountsConfig {
|
||||
pub testnet: bool,
|
||||
pub password_files: Vec<String>,
|
||||
pub unlocked_accounts: Vec<Address>,
|
||||
pub enable_hardware_wallets: bool,
|
||||
}
|
||||
|
||||
impl Default for AccountsConfig {
|
||||
@ -184,6 +185,7 @@ impl Default for AccountsConfig {
|
||||
testnet: false,
|
||||
password_files: Vec::new(),
|
||||
unlocked_accounts: Vec::new(),
|
||||
enable_hardware_wallets: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
use ethcore::ethstore::{PresaleWallet, EthStore};
|
||||
use ethcore::ethstore::dir::RootDiskDirectory;
|
||||
use ethcore::account_provider::AccountProvider;
|
||||
use ethcore::account_provider::{AccountProvider, AccountProviderSettings};
|
||||
use helpers::{password_prompt, password_from_file};
|
||||
use params::SpecType;
|
||||
|
||||
@ -37,7 +37,7 @@ pub fn execute(cmd: ImportWallet) -> Result<String, String> {
|
||||
|
||||
let dir = Box::new(RootDiskDirectory::create(cmd.path).unwrap());
|
||||
let secret_store = Box::new(EthStore::open_with_iterations(dir, cmd.iterations).unwrap());
|
||||
let acc_provider = AccountProvider::new(secret_store);
|
||||
let acc_provider = AccountProvider::new(secret_store, AccountProviderSettings::default());
|
||||
let wallet = PresaleWallet::open(cmd.wallet_path).map_err(|_| "Unable to open presale wallet.")?;
|
||||
let kp = wallet.decrypt(&password).map_err(|_| "Invalid password.")?;
|
||||
let address = acc_provider.insert_account(kp.secret().clone(), &password).unwrap();
|
||||
|
@ -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
|
||||
|
@ -24,6 +24,7 @@ use futures::{future, Future, BoxFuture};
|
||||
use light::client::LightChainClient;
|
||||
use light::on_demand::{request, OnDemand};
|
||||
use light::TransactionQueue as LightTransactionQueue;
|
||||
use rlp::{self, Stream};
|
||||
use util::{Address, H520, H256, U256, Uint, Bytes, RwLock};
|
||||
use util::sha3::Hashable;
|
||||
|
||||
@ -118,7 +119,7 @@ impl<C: MiningBlockChainClient, M: MinerService> Dispatcher for FullDispatcher<C
|
||||
let (client, miner) = (take_weakf!(self.client), take_weakf!(self.miner));
|
||||
let network_id = client.signing_network_id();
|
||||
let address = filled.from;
|
||||
future::ok({
|
||||
future::done({
|
||||
let t = Transaction {
|
||||
nonce: filled.nonce
|
||||
.or_else(|| miner
|
||||
@ -133,13 +134,16 @@ impl<C: MiningBlockChainClient, M: MinerService> Dispatcher for FullDispatcher<C
|
||||
data: filled.data,
|
||||
};
|
||||
|
||||
let hash = t.hash(network_id);
|
||||
let signature = try_bf!(signature(&accounts, address, hash, password));
|
||||
|
||||
signature.map(|sig| {
|
||||
SignedTransaction::new(t.with_signature(sig, network_id))
|
||||
.expect("Transaction was signed by AccountsProvider; it never produces invalid signatures; qed")
|
||||
})
|
||||
if accounts.is_hardware_address(address) {
|
||||
hardware_signature(&*accounts, address, t, network_id).map(WithToken::No)
|
||||
} else {
|
||||
let hash = t.hash(network_id);
|
||||
let signature = try_bf!(signature(&*accounts, address, hash, password));
|
||||
Ok(signature.map(|sig| {
|
||||
SignedTransaction::new(t.with_signature(sig, network_id))
|
||||
.expect("Transaction was signed by AccountsProvider; it never produces invalid signatures; qed")
|
||||
}))
|
||||
}
|
||||
}).boxed()
|
||||
}
|
||||
|
||||
@ -219,8 +223,13 @@ impl Dispatcher for LightDispatcher {
|
||||
value: filled.value,
|
||||
data: filled.data,
|
||||
};
|
||||
|
||||
if accounts.is_hardware_address(address) {
|
||||
return hardware_signature(&*accounts, address, t, network_id).map(WithToken::No)
|
||||
}
|
||||
|
||||
let hash = t.hash(network_id);
|
||||
let signature = signature(&accounts, address, hash, password)?;
|
||||
let signature = signature(&*accounts, address, hash, password)?;
|
||||
|
||||
Ok(signature.map(|sig| {
|
||||
SignedTransaction::new(t.with_signature(sig, network_id))
|
||||
@ -411,6 +420,27 @@ fn signature(accounts: &AccountProvider, address: Address, hash: H256, password:
|
||||
})
|
||||
}
|
||||
|
||||
// obtain a hardware signature from the given account.
|
||||
fn hardware_signature(accounts: &AccountProvider, address: Address, t: Transaction, network_id: Option<u64>)
|
||||
-> Result<SignedTransaction, Error>
|
||||
{
|
||||
debug_assert!(accounts.is_hardware_address(address));
|
||||
|
||||
let mut stream = rlp::RlpStream::new();
|
||||
t.rlp_append_unsigned_transaction(&mut stream, network_id);
|
||||
let signature = accounts.sign_with_hardware(address, &stream.as_raw())
|
||||
.map_err(|e| {
|
||||
debug!(target: "miner", "Error signing transaction with hardware wallet: {}", e);
|
||||
errors::account("Error signing transaction with hardware wallet", e)
|
||||
})?;
|
||||
|
||||
SignedTransaction::new(t.with_signature(signature, network_id))
|
||||
.map_err(|e| {
|
||||
debug!(target: "miner", "Hardware wallet has produced invalid signature: {}", e);
|
||||
errors::account("Invalid signature generated", e)
|
||||
})
|
||||
}
|
||||
|
||||
fn decrypt(accounts: &AccountProvider, address: Address, msg: Bytes, password: SignWith) -> Result<WithToken<Bytes>, Error> {
|
||||
match password.clone() {
|
||||
SignWith::Nothing => accounts.decrypt(address, None, &DEFAULT_MAC, &msg).map(WithToken::No),
|
||||
@ -422,7 +452,7 @@ fn decrypt(accounts: &AccountProvider, address: Address, msg: Bytes, password: S
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract default gas price from a client and miner.
|
||||
/// Extract the default gas price from a client and miner.
|
||||
pub fn default_gas_price<C, M>(client: &C, miner: &M) -> U256
|
||||
where C: MiningBlockChainClient, M: MinerService
|
||||
{
|
||||
|
@ -45,6 +45,7 @@ use v1::types::{
|
||||
TransactionStats, LocalTransactionStatus,
|
||||
BlockNumber, ConsensusCapability, VersionInfo,
|
||||
OperationsInfo, DappId, ChainStatus,
|
||||
AccountInfo, HwAccountInfo
|
||||
};
|
||||
|
||||
/// Parity implementation.
|
||||
@ -111,7 +112,7 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
|
||||
{
|
||||
type Metadata = Metadata;
|
||||
|
||||
fn accounts_info(&self, dapp: Trailing<DappId>) -> Result<BTreeMap<String, BTreeMap<String, String>>, Error> {
|
||||
fn accounts_info(&self, dapp: Trailing<DappId>) -> Result<BTreeMap<H160, AccountInfo>, Error> {
|
||||
let dapp = dapp.0;
|
||||
|
||||
let store = take_weak!(self.accounts);
|
||||
@ -128,12 +129,17 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
|
||||
.into_iter()
|
||||
.chain(other.into_iter())
|
||||
.filter(|&(ref a, _)| dapp_accounts.contains(a))
|
||||
.map(|(a, v)| {
|
||||
let m = map![
|
||||
"name".to_owned() => v.name
|
||||
];
|
||||
(format!("0x{}", a.hex()), m)
|
||||
})
|
||||
.map(|(a, v)| (H160::from(a), AccountInfo { name: v.name }))
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
|
||||
fn hardware_accounts_info(&self) -> Result<BTreeMap<H160, HwAccountInfo>, Error> {
|
||||
let store = take_weak!(self.accounts);
|
||||
let info = store.hardware_accounts_info().map_err(|e| errors::account("Could not fetch account info.", e))?;
|
||||
Ok(info
|
||||
.into_iter()
|
||||
.map(|(a, v)| (H160::from(a), HwAccountInfo { name: v.name, manufacturer: v.meta }))
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use ethcore::account_provider::AccountProvider;
|
||||
use ethcore::account_provider::{AccountProvider, AccountProviderSettings};
|
||||
use ethstore::EthStore;
|
||||
use ethstore::dir::RootDiskDirectory;
|
||||
use devtools::RandomTempPath;
|
||||
@ -36,7 +36,7 @@ fn accounts_provider() -> Arc<AccountProvider> {
|
||||
fn accounts_provider_with_vaults_support(temp_path: &str) -> Arc<AccountProvider> {
|
||||
let root_keys_dir = RootDiskDirectory::create(temp_path).unwrap();
|
||||
let secret_store = EthStore::open(Box::new(root_keys_dir)).unwrap();
|
||||
Arc::new(AccountProvider::new(Box::new(secret_store)))
|
||||
Arc::new(AccountProvider::new(Box::new(secret_store), AccountProviderSettings::default()))
|
||||
}
|
||||
|
||||
fn setup_with_accounts_provider(accounts_provider: Arc<AccountProvider>) -> ParityAccountsTester {
|
||||
@ -314,6 +314,14 @@ fn rpc_parity_vault_adds_vault_field_to_acount_meta() {
|
||||
let response = format!(r#"{{"jsonrpc":"2.0","result":{{"0x{}":{{"meta":"{{\"vault\":\"vault1\"}}","name":"","uuid":"{}"}}}},"id":1}}"#, address1.hex(), uuid1);
|
||||
|
||||
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
|
||||
|
||||
// and then
|
||||
assert!(tester.accounts.change_vault(address1, "").is_ok());
|
||||
|
||||
let request = r#"{"jsonrpc": "2.0", "method": "parity_allAccountsInfo", "params":[], "id": 1}"#;
|
||||
let response = format!(r#"{{"jsonrpc":"2.0","result":{{"0x{}":{{"meta":"{{}}","name":"","uuid":"{}"}}}},"id":1}}"#, address1.hex(), uuid1);
|
||||
|
||||
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -358,6 +366,14 @@ fn rpc_parity_get_set_vault_meta() {
|
||||
let tester = setup_with_vaults_support(temp_path.as_str());
|
||||
|
||||
assert!(tester.accounts.create_vault("vault1", "password1").is_ok());
|
||||
|
||||
// when no meta set
|
||||
let request = r#"{"jsonrpc": "2.0", "method": "parity_getVaultMeta", "params":["vault1"], "id": 1}"#;
|
||||
let response = r#"{"jsonrpc":"2.0","result":"{}","id":1}"#;
|
||||
|
||||
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
|
||||
|
||||
// when meta set
|
||||
assert!(tester.accounts.set_vault_meta("vault1", "vault1_meta").is_ok());
|
||||
|
||||
let request = r#"{"jsonrpc": "2.0", "method": "parity_getVaultMeta", "params":["vault1"], "id": 1}"#;
|
||||
@ -365,11 +381,13 @@ fn rpc_parity_get_set_vault_meta() {
|
||||
|
||||
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
|
||||
|
||||
// change meta
|
||||
let request = r#"{"jsonrpc": "2.0", "method": "parity_setVaultMeta", "params":["vault1", "updated_vault1_meta"], "id": 1}"#;
|
||||
let response = r#"{"jsonrpc":"2.0","result":true,"id":1}"#;
|
||||
|
||||
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
|
||||
|
||||
// query changed meta
|
||||
let request = r#"{"jsonrpc": "2.0", "method": "parity_getVaultMeta", "params":["vault1"], "id": 1}"#;
|
||||
let response = r#"{"jsonrpc":"2.0","result":"updated_vault1_meta","id":1}"#;
|
||||
|
||||
|
@ -28,6 +28,7 @@ use v1::types::{
|
||||
TransactionStats, LocalTransactionStatus,
|
||||
BlockNumber, ConsensusCapability, VersionInfo,
|
||||
OperationsInfo, DappId, ChainStatus,
|
||||
AccountInfo, HwAccountInfo,
|
||||
};
|
||||
|
||||
build_rpc_trait! {
|
||||
@ -37,7 +38,11 @@ build_rpc_trait! {
|
||||
|
||||
/// Returns accounts information.
|
||||
#[rpc(name = "parity_accountsInfo")]
|
||||
fn accounts_info(&self, Trailing<DappId>) -> Result<BTreeMap<String, BTreeMap<String, String>>, Error>;
|
||||
fn accounts_info(&self, Trailing<DappId>) -> Result<BTreeMap<H160, AccountInfo>, Error>;
|
||||
|
||||
/// Returns hardware accounts information.
|
||||
#[rpc(name = "parity_hardwareAccountsInfo")]
|
||||
fn hardware_accounts_info(&self) -> Result<BTreeMap<H160, HwAccountInfo>, Error>;
|
||||
|
||||
/// Returns default account for dapp.
|
||||
#[rpc(meta, name = "parity_defaultAccount")]
|
||||
|
31
rpc/src/v1/types/account_info.rs
Normal file
31
rpc/src/v1/types/account_info.rs
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||
// This file is part of Parity.
|
||||
|
||||
// Parity is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Parity is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/// Account information.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize)]
|
||||
pub struct AccountInfo {
|
||||
/// Account name
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Hardware wallet information.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize)]
|
||||
pub struct HwAccountInfo {
|
||||
/// Device name.
|
||||
pub name: String,
|
||||
/// Device manufacturer.
|
||||
pub manufacturer: String,
|
||||
}
|
@ -14,6 +14,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
mod account_info;
|
||||
mod bytes;
|
||||
mod block;
|
||||
mod block_number;
|
||||
@ -65,3 +66,4 @@ pub use self::uint::{U128, U256};
|
||||
pub use self::work::Work;
|
||||
pub use self::histogram::Histogram;
|
||||
pub use self::consensus_status::*;
|
||||
pub use self::account_info::{AccountInfo, HwAccountInfo};
|
||||
|
@ -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<u8>) -> Result<usize, Inva
|
||||
pub fn validate_compressed_buffer(input: &[u8]) -> bool {
|
||||
let status = unsafe { snappy_validate_compressed_buffer(input.as_ptr() as *const c_char, input.len() as size_t )};
|
||||
status == SNAPPY_OK
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user