Merge branch 'master' into check-updates

This commit is contained in:
Gav Wood 2016-11-18 11:45:04 +08:00
commit 28aabcdb6c
No known key found for this signature in database
GPG Key ID: C49C1ACA1CC9B252
168 changed files with 11973 additions and 8352 deletions

View File

@ -326,7 +326,7 @@ windows:
- set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Include;C:\vs2015\VC\include;C:\Program Files (x86)\Windows Kits\10\Include\10.0.10240.0\ucrt - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Include;C:\vs2015\VC\include;C:\Program Files (x86)\Windows Kits\10\Include\10.0.10240.0\ucrt
- set LIB=C:\vs2015\VC\lib;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10240.0\ucrt\x64 - set LIB=C:\vs2015\VC\lib;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10240.0\ucrt\x64
- set RUST_BACKTRACE=1 - set RUST_BACKTRACE=1
- set RUSTFLAGS=%RUSTFLAGS% -Zorbit=off - set RUSTFLAGS=%RUSTFLAGS%
- rustup default stable-x86_64-pc-windows-msvc - rustup default stable-x86_64-pc-windows-msvc
- cargo build --release %CARGOFLAGS% - cargo build --release %CARGOFLAGS%
- curl -sL --url "https://github.com/ethcore/win-build/raw/master/SimpleFC.dll" -o nsis\SimpleFC.dll - curl -sL --url "https://github.com/ethcore/win-build/raw/master/SimpleFC.dll" -o nsis\SimpleFC.dll

View File

@ -17,7 +17,7 @@ matrix:
include: include:
- rust: stable - rust: stable
env: RUN_TESTS="true" TEST_OPTIONS="--no-release" env: RUN_TESTS="true" TEST_OPTIONS="--no-release"
- rust: beta - rust: stable
env: RUN_COVERAGE="true" env: RUN_COVERAGE="true"
- rust: stable - rust: stable
env: RUN_DOCS="true" env: RUN_DOCS="true"

4
Cargo.lock generated
View File

@ -1250,7 +1250,7 @@ dependencies = [
[[package]] [[package]]
name = "parity-ui-precompiled" name = "parity-ui-precompiled"
version = "1.4.0" version = "1.4.0"
source = "git+https://github.com/ethcore/js-precompiled.git#bc1728a4d24c098ee8792372c14dff98f954518c" source = "git+https://github.com/ethcore/js-precompiled.git#b2513e92603b473799d653583bd86771e0063c08"
dependencies = [ dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@ -1927,7 +1927,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "ws" name = "ws"
version = "0.5.3" version = "0.5.3"
source = "git+https://github.com/ethcore/ws-rs.git?branch=mio-upstream-stable#0cd6c5e3e9d5e61a37d53eb8dcbad523dcc69314" source = "git+https://github.com/ethcore/ws-rs.git?branch=mio-upstream-stable#f5c0b35d660244d1b7500693c8cc28277ce1d418"
dependencies = [ dependencies = [
"bytes 0.4.0-dev (git+https://github.com/carllerche/bytes)", "bytes 0.4.0-dev (git+https://github.com/carllerche/bytes)",
"httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -0,0 +1,42 @@
{
"name": "TestAuthorityRound",
"engine": {
"AuthorityRound": {
"params": {
"gasLimitBoundDivisor": "0x0400",
"stepDuration": "1",
"authorities" : [
"0x7d577a597b2742b498cb5cf0c26cdcd726d39e6e",
"0x82a978b3f5962a5b0957d9ee9eef472ee55b42f1"
]
}
}
},
"params": {
"accountStartNonce": "0x0",
"maximumExtraDataSize": "0x20",
"minGasLimit": "0x1388",
"networkID" : "0x69"
},
"genesis": {
"seal": {
"generic": {
"fields": 1,
"rlp": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"
}
},
"difficulty": "0x20000",
"author": "0x0000000000000000000000000000000000000000",
"timestamp": "0x00",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"extraData": "0x",
"gasLimit": "0x2fefd8"
},
"accounts": {
"0000000000000000000000000000000000000001": { "balance": "1", "nonce": "1048576", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },
"0000000000000000000000000000000000000002": { "balance": "1", "nonce": "1048576", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } },
"0000000000000000000000000000000000000003": { "balance": "1", "nonce": "1048576", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } },
"0000000000000000000000000000000000000004": { "balance": "1", "nonce": "1048576", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } },
"9cce34f7ab185c7aba1b7c8140d620b4bda941d6": { "balance": "1606938044258990275541962092341162602522202993782792835301376", "nonce": "1048576" }
}
}

View File

@ -1,5 +1,5 @@
{ {
"name": "TestAuthority", "name": "TestBasicAuthority",
"engine": { "engine": {
"BasicAuthority": { "BasicAuthority": {
"params": { "params": {

View File

@ -131,10 +131,11 @@
"0x807640a13483f8ac783c557fcdf27be11ea4ac7a" "0x807640a13483f8ac783c557fcdf27be11ea4ac7a"
], ],
"eip150Transition": "0x259518", "eip150Transition": "0x259518",
"eip155Transition": "0x7fffffffffffffff", "eip155Transition": 2675000,
"eip160Transition": "0x7fffffffffffffff", "eip160Transition": 2675000,
"eip161abcTransition": "0x7fffffffffffffff", "eip161abcTransition": 2675000,
"eip161dTransition": "0x7fffffffffffffff" "eip161dTransition": 2675000,
"maxCodeSize": 24576
} }
} }
}, },

View File

@ -11,10 +11,10 @@
"registrar": "0x52dff57a8a1532e6afb3dc07e2af58bb9eb05b3d", "registrar": "0x52dff57a8a1532e6afb3dc07e2af58bb9eb05b3d",
"homesteadTransition": "0x789b0", "homesteadTransition": "0x789b0",
"eip150Transition": "0x1b34d8", "eip150Transition": "0x1b34d8",
"eip155Transition": "0x7fffffffffffffff", "eip155Transition": 1885000,
"eip160Transition": "0x7fffffffffffffff", "eip160Transition": 1885000,
"eip161abcTransition": "0x7fffffffffffffff", "eip161abcTransition": 1885000,
"eip161dTransition": "0x7fffffffffffffff" "eip161dTransition": 1885000
} }
} }
}, },

View File

@ -1,5 +1,5 @@
{ {
"name": "TestInstantSeal", "name": "DevelopmentChain",
"engine": { "engine": {
"InstantSeal": null "InstantSeal": null
}, },
@ -28,6 +28,6 @@
"0000000000000000000000000000000000000002": { "balance": "1", "nonce": "1048576", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } }, "0000000000000000000000000000000000000002": { "balance": "1", "nonce": "1048576", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } },
"0000000000000000000000000000000000000003": { "balance": "1", "nonce": "1048576", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } }, "0000000000000000000000000000000000000003": { "balance": "1", "nonce": "1048576", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } },
"0000000000000000000000000000000000000004": { "balance": "1", "nonce": "1048576", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } }, "0000000000000000000000000000000000000004": { "balance": "1", "nonce": "1048576", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } },
"102e61f5d8f9bc71d0ad4a084df4e65e05ce0e1c": { "balance": "1606938044258990275541962092341162602522202993782792835301376", "nonce": "1048576" } "0x00a329c0648769a73afac7f9381e08fb43dbea72": { "balance": "1606938044258990275541962092341162602522202993782792835301376", "nonce": "1048576" }
} }
} }

View File

@ -556,6 +556,7 @@ impl Client {
/// Import transactions from the IO queue /// Import transactions from the IO queue
pub fn import_queued_transactions(&self, transactions: &[Bytes]) -> usize { pub fn import_queued_transactions(&self, transactions: &[Bytes]) -> usize {
trace!(target: "external_tx", "Importing queued");
let _timer = PerfTimer::new("import_queued_transactions"); let _timer = PerfTimer::new("import_queued_transactions");
self.queue_transactions.fetch_sub(transactions.len(), AtomicOrdering::SeqCst); self.queue_transactions.fetch_sub(transactions.len(), AtomicOrdering::SeqCst);
let txs = transactions.iter().filter_map(|bytes| UntrustedRlp::new(bytes).as_val().ok()).collect(); let txs = transactions.iter().filter_map(|bytes| UntrustedRlp::new(bytes).as_val().ok()).collect();
@ -563,6 +564,11 @@ impl Client {
results.len() results.len()
} }
/// Used by PoA to try sealing on period change.
pub fn update_sealing(&self) {
self.miner.update_sealing(self)
}
/// Attempt to get a copy of a specific block's final state. /// Attempt to get a copy of a specific block's final state.
/// ///
/// This will not fail if given BlockID::Latest. /// This will not fail if given BlockID::Latest.
@ -1254,7 +1260,9 @@ impl BlockChainClient for Client {
} }
fn queue_transactions(&self, transactions: Vec<Bytes>) { fn queue_transactions(&self, transactions: Vec<Bytes>) {
if self.queue_transactions.load(AtomicOrdering::Relaxed) > MAX_TX_QUEUE_SIZE { let queue_size = self.queue_transactions.load(AtomicOrdering::Relaxed);
trace!(target: "external_tx", "Queue size: {}", queue_size);
if queue_size > MAX_TX_QUEUE_SIZE {
debug!("Ignoring {} transactions: queue is full", transactions.len()); debug!("Ignoring {} transactions: queue is full", transactions.len());
} else { } else {
let len = transactions.len(); let len = transactions.len();

View File

@ -119,6 +119,16 @@ impl TestBlockChainClient {
/// Creates new test client with specified extra data for each block /// Creates new test client with specified extra data for each block
pub fn new_with_extra_data(extra_data: Bytes) -> Self { pub fn new_with_extra_data(extra_data: Bytes) -> Self {
let spec = Spec::new_test(); let spec = Spec::new_test();
TestBlockChainClient::new_with_spec_and_extra(spec, extra_data)
}
/// Create test client with custom spec.
pub fn new_with_spec(spec: Spec) -> Self {
TestBlockChainClient::new_with_spec_and_extra(spec, Bytes::new())
}
/// Create test client with custom spec and extra data.
pub fn new_with_spec_and_extra(spec: Spec, extra_data: Bytes) -> Self {
let mut client = TestBlockChainClient { let mut client = TestBlockChainClient {
blocks: RwLock::new(HashMap::new()), blocks: RwLock::new(HashMap::new()),
numbers: RwLock::new(HashMap::new()), numbers: RwLock::new(HashMap::new()),
@ -315,7 +325,7 @@ pub fn get_temp_state_db() -> GuardedTempResult<StateDB> {
impl MiningBlockChainClient for TestBlockChainClient { impl MiningBlockChainClient for TestBlockChainClient {
fn latest_schedule(&self) -> Schedule { fn latest_schedule(&self) -> Schedule {
Schedule::new_post_eip150(true, true, true) Schedule::new_post_eip150(24576, true, true, true)
} }
fn prepare_open_block(&self, author: Address, gas_range_target: (U256, U256), extra_data: Bytes) -> OpenBlock { fn prepare_open_block(&self, author: Address, gas_range_target: (U256, U256), extra_data: Bytes) -> OpenBlock {

View File

@ -0,0 +1,429 @@
// Copyright 2015, 2016 Ethcore (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/>.
//! A blockchain engine that supports a non-instant BFT proof-of-authority.
use std::sync::atomic::{AtomicUsize, AtomicBool, Ordering as AtomicOrdering};
use std::sync::Weak;
use std::time::{UNIX_EPOCH, Duration};
use util::*;
use ethkey::{verify_address, Signature};
use rlp::{UntrustedRlp, View, encode};
use account_provider::AccountProvider;
use block::*;
use spec::CommonParams;
use engines::Engine;
use header::Header;
use error::{Error, BlockError};
use evm::Schedule;
use ethjson;
use io::{IoContext, IoHandler, TimerToken, IoService, IoChannel};
use service::ClientIoMessage;
use transaction::SignedTransaction;
use env_info::EnvInfo;
use builtin::Builtin;
/// `AuthorityRound` params.
#[derive(Debug, PartialEq)]
pub struct AuthorityRoundParams {
/// Gas limit divisor.
pub gas_limit_bound_divisor: U256,
/// Time to wait before next block or authority switching.
pub step_duration: Duration,
/// Valid authorities.
pub authorities: Vec<Address>,
/// Number of authorities.
pub authority_n: usize,
}
impl From<ethjson::spec::AuthorityRoundParams> for AuthorityRoundParams {
fn from(p: ethjson::spec::AuthorityRoundParams) -> Self {
AuthorityRoundParams {
gas_limit_bound_divisor: p.gas_limit_bound_divisor.into(),
step_duration: Duration::from_secs(p.step_duration.into()),
authority_n: p.authorities.len(),
authorities: p.authorities.into_iter().map(Into::into).collect::<Vec<_>>(),
}
}
}
/// Engine using `AuthorityRound` proof-of-work consensus algorithm, suitable for Ethereum
/// mainnet chains in the Olympic, Frontier and Homestead eras.
pub struct AuthorityRound {
params: CommonParams,
our_params: AuthorityRoundParams,
builtins: BTreeMap<Address, Builtin>,
transition_service: IoService<BlockArrived>,
message_channel: Mutex<Option<IoChannel<ClientIoMessage>>>,
step: AtomicUsize,
proposed: AtomicBool,
}
fn header_step(header: &Header) -> Result<usize, ::rlp::DecoderError> {
UntrustedRlp::new(&header.seal()[0]).as_val()
}
fn header_signature(header: &Header) -> Result<Signature, ::rlp::DecoderError> {
UntrustedRlp::new(&header.seal()[1]).as_val::<H520>().map(Into::into)
}
trait AsMillis {
fn as_millis(&self) -> u64;
}
impl AsMillis for Duration {
fn as_millis(&self) -> u64 {
self.as_secs()*1_000 + (self.subsec_nanos()/1_000_000) as u64
}
}
impl AuthorityRound {
/// Create a new instance of AuthorityRound engine.
pub fn new(params: CommonParams, our_params: AuthorityRoundParams, builtins: BTreeMap<Address, Builtin>) -> Result<Arc<Self>, Error> {
let initial_step = (unix_now().as_secs() / our_params.step_duration.as_secs()) as usize;
let engine = Arc::new(
AuthorityRound {
params: params,
our_params: our_params,
builtins: builtins,
transition_service: try!(IoService::<BlockArrived>::start()),
message_channel: Mutex::new(None),
step: AtomicUsize::new(initial_step),
proposed: AtomicBool::new(false)
});
let handler = TransitionHandler { engine: Arc::downgrade(&engine) };
try!(engine.transition_service.register_handler(Arc::new(handler)));
Ok(engine)
}
fn step(&self) -> usize {
self.step.load(AtomicOrdering::SeqCst)
}
fn remaining_step_duration(&self) -> Duration {
let now = unix_now();
let step_end = self.our_params.step_duration * (self.step() as u32 + 1);
if step_end > now {
step_end - now
} else {
Duration::from_secs(0)
}
}
fn step_proposer(&self, step: usize) -> &Address {
let ref p = self.our_params;
p.authorities.get(step % p.authority_n).expect("There are authority_n authorities; taking number modulo authority_n gives number in authority_n range; qed")
}
fn is_step_proposer(&self, step: usize, address: &Address) -> bool {
self.step_proposer(step) == address
}
}
fn unix_now() -> Duration {
UNIX_EPOCH.elapsed().expect("Valid time has to be set in your system.")
}
struct TransitionHandler {
engine: Weak<AuthorityRound>,
}
#[derive(Clone)]
struct BlockArrived;
const ENGINE_TIMEOUT_TOKEN: TimerToken = 23;
impl IoHandler<BlockArrived> for TransitionHandler {
fn initialize(&self, io: &IoContext<BlockArrived>) {
if let Some(engine) = self.engine.upgrade() {
io.register_timer_once(ENGINE_TIMEOUT_TOKEN, engine.remaining_step_duration().as_millis())
.unwrap_or_else(|e| warn!(target: "poa", "Failed to start consensus step timer: {}.", e))
}
}
fn timeout(&self, io: &IoContext<BlockArrived>, timer: TimerToken) {
if timer == ENGINE_TIMEOUT_TOKEN {
if let Some(engine) = self.engine.upgrade() {
engine.step.fetch_add(1, AtomicOrdering::SeqCst);
engine.proposed.store(false, AtomicOrdering::SeqCst);
if let Some(ref channel) = *engine.message_channel.lock() {
match channel.send(ClientIoMessage::UpdateSealing) {
Ok(_) => trace!(target: "poa", "timeout: UpdateSealing message sent for step {}.", engine.step.load(AtomicOrdering::Relaxed)),
Err(err) => trace!(target: "poa", "timeout: Could not send a sealing message {} for step {}.", err, engine.step.load(AtomicOrdering::Relaxed)),
}
}
io.register_timer_once(ENGINE_TIMEOUT_TOKEN, engine.remaining_step_duration().as_millis())
.unwrap_or_else(|e| warn!(target: "poa", "Failed to restart consensus step timer: {}.", e))
}
}
}
}
impl Engine for AuthorityRound {
fn name(&self) -> &str { "AuthorityRound" }
fn version(&self) -> SemanticVersion { SemanticVersion::new(1, 0, 0) }
/// Two fields - consensus step and the corresponding proposer signature.
fn seal_fields(&self) -> usize { 2 }
fn params(&self) -> &CommonParams { &self.params }
fn builtins(&self) -> &BTreeMap<Address, Builtin> { &self.builtins }
/// Additional engine-specific information for the user/developer concerning `header`.
fn extra_info(&self, header: &Header) -> BTreeMap<String, String> {
map![
"step".into() => header_step(header).as_ref().map(ToString::to_string).unwrap_or("".into()),
"signature".into() => header_signature(header).as_ref().map(ToString::to_string).unwrap_or("".into())
]
}
fn schedule(&self, _env_info: &EnvInfo) -> Schedule {
Schedule::new_post_eip150(usize::max_value(), true, true, true)
}
fn populate_from_parent(&self, header: &mut Header, parent: &Header, gas_floor_target: U256, _gas_ceil_target: U256) {
header.set_difficulty(parent.difficulty().clone());
header.set_gas_limit({
let gas_limit = parent.gas_limit().clone();
let bound_divisor = self.our_params.gas_limit_bound_divisor;
if gas_limit < gas_floor_target {
min(gas_floor_target, gas_limit + gas_limit / bound_divisor - 1.into())
} else {
max(gas_floor_target, gas_limit - gas_limit / bound_divisor + 1.into())
}
});
}
/// Apply the block reward on finalisation of the block.
/// This assumes that all uncles are valid uncles (i.e. of at least one generation before the current).
fn on_close_block(&self, _block: &mut ExecutedBlock) {}
fn is_sealer(&self, author: &Address) -> Option<bool> {
let ref p = self.our_params;
Some(p.authorities.contains(author))
}
/// Attempt to seal the block internally.
///
/// This operation is synchronous and may (quite reasonably) not be available, in which `false` will
/// be returned.
fn generate_seal(&self, block: &ExecutedBlock, accounts: Option<&AccountProvider>) -> Option<Vec<Bytes>> {
if self.proposed.load(AtomicOrdering::SeqCst) { return None; }
let header = block.header();
let step = self.step();
if self.is_step_proposer(step, header.author()) {
if let Some(ap) = accounts {
// Account should be permanently unlocked, otherwise sealing will fail.
if let Ok(signature) = ap.sign(*header.author(), None, header.bare_hash()) {
trace!(target: "poa", "generate_seal: Issuing a block for step {}.", step);
self.proposed.store(true, AtomicOrdering::SeqCst);
return Some(vec![encode(&step).to_vec(), encode(&(&*signature as &[u8])).to_vec()]);
} else {
warn!(target: "poa", "generate_seal: FAIL: Accounts secret key unavailable.");
}
} else {
warn!(target: "poa", "generate_seal: FAIL: Accounts not provided.");
}
}
None
}
/// Check the number of seal fields.
fn verify_block_basic(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
if header.seal().len() != self.seal_fields() {
trace!(target: "poa", "verify_block_basic: wrong number of seal fields");
Err(From::from(BlockError::InvalidSealArity(
Mismatch { expected: self.seal_fields(), found: header.seal().len() }
)))
} else {
Ok(())
}
}
/// Check if the signature belongs to the correct proposer.
fn verify_block_unordered(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
let header_step = try!(header_step(header));
// Give one step slack if step is lagging, double vote is still not possible.
if header_step <= self.step() + 1 {
let proposer_signature = try!(header_signature(header));
let ok_sig = try!(verify_address(self.step_proposer(header_step), &proposer_signature, &header.bare_hash()));
if ok_sig {
Ok(())
} else {
trace!(target: "poa", "verify_block_unordered: invalid seal signature");
try!(Err(BlockError::InvalidSeal))
}
} else {
trace!(target: "poa", "verify_block_unordered: block from the future");
try!(Err(BlockError::InvalidSeal))
}
}
fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
// Don't calculate difficulty for genesis blocks.
if header.number() == 0 {
return Err(From::from(BlockError::RidiculousNumber(OutOfBounds { min: Some(1), max: None, found: header.number() })));
}
let step = try!(header_step(header));
// Check if parent is from a previous step.
if step == try!(header_step(parent)) {
trace!(target: "poa", "Multiple blocks proposed for step {}.", step);
try!(Err(BlockError::DoubleVote(header.author().clone())));
}
// Check difficulty is correct given the two timestamps.
if header.difficulty() != parent.difficulty() {
return Err(From::from(BlockError::InvalidDifficulty(Mismatch { expected: *parent.difficulty(), found: *header.difficulty() })))
}
let gas_limit_divisor = self.our_params.gas_limit_bound_divisor;
let min_gas = parent.gas_limit().clone() - parent.gas_limit().clone() / gas_limit_divisor;
let max_gas = parent.gas_limit().clone() + parent.gas_limit().clone() / gas_limit_divisor;
if header.gas_limit() <= &min_gas || header.gas_limit() >= &max_gas {
return Err(From::from(BlockError::InvalidGasLimit(OutOfBounds { min: Some(min_gas), max: Some(max_gas), found: header.gas_limit().clone() })));
}
Ok(())
}
fn verify_transaction_basic(&self, t: &SignedTransaction, _header: &Header) -> Result<(), Error> {
try!(t.check_low_s());
Ok(())
}
fn verify_transaction(&self, t: &SignedTransaction, _header: &Header) -> Result<(), Error> {
t.sender().map(|_|()) // Perform EC recovery and cache sender
}
fn register_message_channel(&self, message_channel: IoChannel<ClientIoMessage>) {
let mut guard = self.message_channel.lock();
*guard = Some(message_channel);
}
}
#[cfg(test)]
mod tests {
use util::*;
use env_info::EnvInfo;
use header::Header;
use error::{Error, BlockError};
use rlp::encode;
use block::*;
use tests::helpers::*;
use account_provider::AccountProvider;
use spec::Spec;
use std::time::UNIX_EPOCH;
#[test]
fn has_valid_metadata() {
let engine = Spec::new_test_round().engine;
assert!(!engine.name().is_empty());
assert!(engine.version().major >= 1);
}
#[test]
fn can_return_schedule() {
let engine = Spec::new_test_round().engine;
let schedule = engine.schedule(&EnvInfo {
number: 10000000,
author: 0.into(),
timestamp: 0,
difficulty: 0.into(),
last_hashes: Arc::new(vec![]),
gas_used: 0.into(),
gas_limit: 0.into(),
});
assert!(schedule.stack_limit > 0);
}
#[test]
fn verification_fails_on_short_seal() {
let engine = Spec::new_test_round().engine;
let header: Header = Header::default();
let verify_result = engine.verify_block_basic(&header, None);
match verify_result {
Err(Error::Block(BlockError::InvalidSealArity(_))) => {},
Err(_) => { panic!("should be block seal-arity mismatch error (got {:?})", verify_result); },
_ => { panic!("Should be error, got Ok"); },
}
}
#[test]
fn can_do_signature_verification_fail() {
let engine = Spec::new_test_round().engine;
let mut header: Header = Header::default();
header.set_seal(vec![encode(&H520::default()).to_vec()]);
let verify_result = engine.verify_block_unordered(&header, None);
assert!(verify_result.is_err());
}
#[test]
fn generates_seal_and_does_not_double_propose() {
let tap = AccountProvider::transient_provider();
let addr1 = tap.insert_account("1".sha3(), "1").unwrap();
tap.unlock_account_permanently(addr1, "1".into()).unwrap();
let addr2 = tap.insert_account("2".sha3(), "2").unwrap();
tap.unlock_account_permanently(addr2, "2".into()).unwrap();
let spec = Spec::new_test_round();
let engine = &*spec.engine;
let genesis_header = spec.genesis_header();
let mut db1 = get_temp_state_db().take();
spec.ensure_db_good(&mut db1).unwrap();
let mut db2 = get_temp_state_db().take();
spec.ensure_db_good(&mut db2).unwrap();
let last_hashes = Arc::new(vec![genesis_header.hash()]);
let b1 = OpenBlock::new(engine, Default::default(), false, db1, &genesis_header, last_hashes.clone(), addr1, (3141562.into(), 31415620.into()), vec![]).unwrap();
let b1 = b1.close_and_lock();
let b2 = OpenBlock::new(engine, Default::default(), false, db2, &genesis_header, last_hashes, addr2, (3141562.into(), 31415620.into()), vec![]).unwrap();
let b2 = b2.close_and_lock();
if let Some(seal) = engine.generate_seal(b1.block(), Some(&tap)) {
assert!(b1.clone().try_seal(engine, seal).is_ok());
// Second proposal is forbidden.
assert!(engine.generate_seal(b1.block(), Some(&tap)).is_none());
}
if let Some(seal) = engine.generate_seal(b2.block(), Some(&tap)) {
assert!(b2.clone().try_seal(engine, seal).is_ok());
// Second proposal is forbidden.
assert!(engine.generate_seal(b2.block(), Some(&tap)).is_none());
}
}
#[test]
fn proposer_switching() {
let mut header: Header = Header::default();
let tap = AccountProvider::transient_provider();
let addr = tap.insert_account("0".sha3(), "0").unwrap();
header.set_author(addr);
let engine = Spec::new_test_round().engine;
let signature = tap.sign(addr, Some("0".into()), header.bare_hash()).unwrap();
let mut step = UNIX_EPOCH.elapsed().unwrap().as_secs();
header.set_seal(vec![encode(&step).to_vec(), encode(&(&*signature as &[u8])).to_vec()]);
let first_ok = engine.verify_block_seal(&header).is_ok();
step = step + 1;
header.set_seal(vec![encode(&step).to_vec(), encode(&(&*signature as &[u8])).to_vec()]);
let second_ok = engine.verify_block_seal(&header).is_ok();
assert!(first_ok ^ second_ok);
}
}

View File

@ -181,13 +181,6 @@ impl Engine for BasicAuthority {
} }
} }
impl Header {
/// Get the none field of the header.
pub fn signature(&self) -> H520 {
::rlp::decode(&self.seal()[0])
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use util::*; use util::*;
@ -201,7 +194,7 @@ mod tests {
/// Create a new test chain spec with `BasicAuthority` consensus engine. /// Create a new test chain spec with `BasicAuthority` consensus engine.
fn new_test_authority() -> Spec { fn new_test_authority() -> Spec {
let bytes: &[u8] = include_bytes!("../../res/test_authority.json"); let bytes: &[u8] = include_bytes!("../../res/basic_authority.json");
Spec::load(bytes).expect("invalid chain spec") Spec::load(bytes).expect("invalid chain spec")
} }

View File

@ -55,7 +55,7 @@ impl Engine for InstantSeal {
} }
fn schedule(&self, _env_info: &EnvInfo) -> Schedule { fn schedule(&self, _env_info: &EnvInfo) -> Schedule {
Schedule::new_homestead() Schedule::new_post_eip150(usize::max_value(), false, false, false)
} }
fn is_sealer(&self, _author: &Address) -> Option<bool> { Some(true) } fn is_sealer(&self, _author: &Address) -> Option<bool> { Some(true) }
@ -79,7 +79,7 @@ mod tests {
let tap = AccountProvider::transient_provider(); let tap = AccountProvider::transient_provider();
let addr = tap.insert_account("".sha3(), "").unwrap(); let addr = tap.insert_account("".sha3(), "").unwrap();
let spec = Spec::new_test_instant(); let spec = Spec::new_instant();
let engine = &*spec.engine; let engine = &*spec.engine;
let genesis_header = spec.genesis_header(); let genesis_header = spec.genesis_header();
let mut db_result = get_temp_state_db(); let mut db_result = get_temp_state_db();
@ -95,7 +95,7 @@ mod tests {
#[test] #[test]
fn instant_cant_verify() { fn instant_cant_verify() {
let engine = Spec::new_test_instant().engine; let engine = Spec::new_instant().engine;
let mut header: Header = Header::default(); let mut header: Header = Header::default();
assert!(engine.verify_block_basic(&header, None).is_ok()); assert!(engine.verify_block_basic(&header, None).is_ok());

View File

@ -19,10 +19,12 @@
mod null_engine; mod null_engine;
mod instant_seal; mod instant_seal;
mod basic_authority; mod basic_authority;
mod authority_round;
pub use self::null_engine::NullEngine; pub use self::null_engine::NullEngine;
pub use self::instant_seal::InstantSeal; pub use self::instant_seal::InstantSeal;
pub use self::basic_authority::BasicAuthority; pub use self::basic_authority::BasicAuthority;
pub use self::authority_round::AuthorityRound;
use util::*; use util::*;
use account_provider::AccountProvider; use account_provider::AccountProvider;
@ -32,6 +34,8 @@ use env_info::EnvInfo;
use error::Error; use error::Error;
use spec::CommonParams; use spec::CommonParams;
use evm::Schedule; use evm::Schedule;
use io::IoChannel;
use service::ClientIoMessage;
use header::Header; use header::Header;
use transaction::SignedTransaction; use transaction::SignedTransaction;
@ -140,5 +144,7 @@ pub trait Engine : Sync + Send {
self.builtins().get(a).expect("attempted to execute nonexistent builtin").execute(input, output); self.builtins().get(a).expect("attempted to execute nonexistent builtin").execute(input, output);
} }
/// Add a channel for communication with Client which can be used for sealing.
fn register_message_channel(&self, _message_channel: IoChannel<ClientIoMessage>) {}
// TODO: sealing stuff - though might want to leave this for later. // TODO: sealing stuff - though might want to leave this for later.
} }

View File

@ -167,6 +167,8 @@ pub enum BlockError {
UnknownParent(H256), UnknownParent(H256),
/// Uncle parent given is unknown. /// Uncle parent given is unknown.
UnknownUncleParent(H256), UnknownUncleParent(H256),
/// The same author issued different votes at the same step.
DoubleVote(H160),
} }
impl fmt::Display for BlockError { impl fmt::Display for BlockError {
@ -200,6 +202,7 @@ impl fmt::Display for BlockError {
RidiculousNumber(ref oob) => format!("Implausible block number. {}", oob), RidiculousNumber(ref oob) => format!("Implausible block number. {}", oob),
UnknownParent(ref hash) => format!("Unknown parent: {}", hash), UnknownParent(ref hash) => format!("Unknown parent: {}", hash),
UnknownUncleParent(ref hash) => format!("Unknown uncle parent: {}", hash), UnknownUncleParent(ref hash) => format!("Unknown uncle parent: {}", hash),
DoubleVote(ref address) => format!("Author {} issued too many blocks.", address),
}; };
f.write_fmt(format_args!("Block error ({})", msg)) f.write_fmt(format_args!("Block error ({})", msg))

View File

@ -73,7 +73,9 @@ pub struct EthashParams {
/// Number of first block where ECIP-1010 begins. /// Number of first block where ECIP-1010 begins.
pub ecip1010_pause_transition: u64, pub ecip1010_pause_transition: u64,
/// Number of first block where ECIP-1010 ends. /// Number of first block where ECIP-1010 ends.
pub ecip1010_continue_transition: u64 pub ecip1010_continue_transition: u64,
/// Maximum amount of code that can be deploying into a contract.
pub max_code_size: u64,
} }
impl From<ethjson::spec::EthashParams> for EthashParams { impl From<ethjson::spec::EthashParams> for EthashParams {
@ -87,19 +89,20 @@ impl From<ethjson::spec::EthashParams> for EthashParams {
block_reward: p.block_reward.into(), block_reward: p.block_reward.into(),
registrar: p.registrar.map_or_else(Address::new, Into::into), registrar: p.registrar.map_or_else(Address::new, Into::into),
homestead_transition: p.homestead_transition.map_or(0, Into::into), homestead_transition: p.homestead_transition.map_or(0, Into::into),
dao_hardfork_transition: p.dao_hardfork_transition.map_or(0x7fffffffffffffff, Into::into), dao_hardfork_transition: p.dao_hardfork_transition.map_or(u64::max_value(), Into::into),
dao_hardfork_beneficiary: p.dao_hardfork_beneficiary.map_or_else(Address::new, Into::into), dao_hardfork_beneficiary: p.dao_hardfork_beneficiary.map_or_else(Address::new, Into::into),
dao_hardfork_accounts: p.dao_hardfork_accounts.unwrap_or_else(Vec::new).into_iter().map(Into::into).collect(), dao_hardfork_accounts: p.dao_hardfork_accounts.unwrap_or_else(Vec::new).into_iter().map(Into::into).collect(),
difficulty_hardfork_transition: p.difficulty_hardfork_transition.map_or(0x7fffffffffffffff, Into::into), difficulty_hardfork_transition: p.difficulty_hardfork_transition.map_or(u64::max_value(), Into::into),
difficulty_hardfork_bound_divisor: p.difficulty_hardfork_bound_divisor.map_or(p.difficulty_bound_divisor.into(), Into::into), difficulty_hardfork_bound_divisor: p.difficulty_hardfork_bound_divisor.map_or(p.difficulty_bound_divisor.into(), Into::into),
bomb_defuse_transition: p.bomb_defuse_transition.map_or(0x7fffffffffffffff, Into::into), bomb_defuse_transition: p.bomb_defuse_transition.map_or(u64::max_value(), Into::into),
eip150_transition: p.eip150_transition.map_or(0, Into::into), eip150_transition: p.eip150_transition.map_or(0, Into::into),
eip155_transition: p.eip155_transition.map_or(0, Into::into), eip155_transition: p.eip155_transition.map_or(0, Into::into),
eip160_transition: p.eip160_transition.map_or(0, Into::into), eip160_transition: p.eip160_transition.map_or(0, Into::into),
eip161abc_transition: p.eip161abc_transition.map_or(0, Into::into), eip161abc_transition: p.eip161abc_transition.map_or(0, Into::into),
eip161d_transition: p.eip161d_transition.map_or(0x7fffffffffffffff, Into::into), eip161d_transition: p.eip161d_transition.map_or(u64::max_value(), Into::into),
ecip1010_pause_transition: p.ecip1010_pause_transition.map_or(0x7fffffffffffffff, Into::into), ecip1010_pause_transition: p.ecip1010_pause_transition.map_or(u64::max_value(), Into::into),
ecip1010_continue_transition: p.ecip1010_continue_transition.map_or(0x7fffffffffffffff, Into::into), ecip1010_continue_transition: p.ecip1010_continue_transition.map_or(u64::max_value(), Into::into),
max_code_size: p.max_code_size.map_or(u64::max_value(), Into::into),
} }
} }
} }
@ -152,6 +155,7 @@ impl Engine for Ethash {
Schedule::new_homestead() Schedule::new_homestead()
} else { } else {
Schedule::new_post_eip150( Schedule::new_post_eip150(
self.ethash_params.max_code_size as usize,
env_info.number >= self.ethash_params.eip160_transition, env_info.number >= self.ethash_params.eip160_transition,
env_info.number >= self.ethash_params.eip161abc_transition, env_info.number >= self.ethash_params.eip161abc_transition,
env_info.number >= self.ethash_params.eip161d_transition env_info.number >= self.ethash_params.eip161d_transition

View File

@ -70,6 +70,8 @@ pub struct Schedule {
pub quad_coeff_div: usize, pub quad_coeff_div: usize,
/// Cost for contract length when executing `CREATE` /// Cost for contract length when executing `CREATE`
pub create_data_gas: usize, pub create_data_gas: usize,
/// Maximum code size when creating a contract.
pub create_data_limit: usize,
/// Transaction cost /// Transaction cost
pub tx_gas: usize, pub tx_gas: usize,
/// `CREATE` transaction cost /// `CREATE` transaction cost
@ -111,7 +113,7 @@ impl Schedule {
} }
/// Schedule for the post-EIP-150-era of the Ethereum main net. /// Schedule for the post-EIP-150-era of the Ethereum main net.
pub fn new_post_eip150(fix_exp: bool, no_empty: bool, kill_empty: bool) -> Schedule { pub fn new_post_eip150(max_code_size: usize, fix_exp: bool, no_empty: bool, kill_empty: bool) -> Schedule {
Schedule { Schedule {
exceptional_failed_code_deposit: true, exceptional_failed_code_deposit: true,
have_delegate_call: true, have_delegate_call: true,
@ -139,6 +141,7 @@ impl Schedule {
memory_gas: 3, memory_gas: 3,
quad_coeff_div: 512, quad_coeff_div: 512,
create_data_gas: 200, create_data_gas: 200,
create_data_limit: max_code_size,
tx_gas: 21000, tx_gas: 21000,
tx_create_gas: 53000, tx_create_gas: 53000,
tx_data_zero_gas: 4, tx_data_zero_gas: 4,
@ -183,6 +186,7 @@ impl Schedule {
memory_gas: 3, memory_gas: 3,
quad_coeff_div: 512, quad_coeff_div: 512,
create_data_gas: 200, create_data_gas: 200,
create_data_limit: usize::max_value(),
tx_gas: 21000, tx_gas: 21000,
tx_create_gas: tcg, tx_create_gas: tcg,
tx_data_zero_gas: 4, tx_data_zero_gas: 4,

View File

@ -242,7 +242,7 @@ impl<'a, T, V> Ext for Externalities<'a, T, V> where T: 'a + Tracer, V: 'a + VMT
}, },
OutputPolicy::InitContract(ref mut copy) => { OutputPolicy::InitContract(ref mut copy) => {
let return_cost = U256::from(data.len()) * U256::from(self.schedule.create_data_gas); let return_cost = U256::from(data.len()) * U256::from(self.schedule.create_data_gas);
if return_cost > *gas { if return_cost > *gas || data.len() > self.schedule.create_data_limit {
return match self.schedule.exceptional_failed_code_deposit { return match self.schedule.exceptional_failed_code_deposit {
true => Err(evm::Error::OutOfGas), true => Err(evm::Error::OutOfGas),
false => Ok(*gas) false => Ok(*gas)

View File

@ -212,7 +212,8 @@ pub struct Miner {
sealing_block_last_request: Mutex<u64>, sealing_block_last_request: Mutex<u64>,
// for sealing... // for sealing...
options: MinerOptions, options: MinerOptions,
seals_internally: bool, /// Does the node perform internal (without work) sealing.
pub seals_internally: bool,
gas_range_target: RwLock<(U256, U256)>, gas_range_target: RwLock<(U256, U256)>,
author: RwLock<Address>, author: RwLock<Address>,
@ -267,6 +268,11 @@ impl Miner {
} }
} }
/// Creates new instance of miner with accounts and with given spec.
pub fn with_spec_and_accounts(spec: &Spec, accounts: Option<Arc<AccountProvider>>) -> Miner {
Miner::new_raw(Default::default(), GasPricer::new_fixed(20_000_000_000u64.into()), spec, accounts)
}
/// Creates new instance of miner without accounts, but with given spec. /// Creates new instance of miner without accounts, but with given spec.
pub fn with_spec(spec: &Spec) -> Miner { pub fn with_spec(spec: &Spec) -> Miner {
Miner::new_raw(Default::default(), GasPricer::new_fixed(20_000_000_000u64.into()), spec, None) Miner::new_raw(Default::default(), GasPricer::new_fixed(20_000_000_000u64.into()), spec, None)
@ -429,6 +435,7 @@ impl Miner {
let last_request = *self.sealing_block_last_request.lock(); let last_request = *self.sealing_block_last_request.lock();
let should_disable_sealing = !self.forced_sealing() let should_disable_sealing = !self.forced_sealing()
&& !has_local_transactions && !has_local_transactions
&& !self.seals_internally
&& best_block > last_request && best_block > last_request
&& best_block - last_request > SEALING_TIMEOUT_IN_BLOCKS; && best_block - last_request > SEALING_TIMEOUT_IN_BLOCKS;
@ -472,9 +479,10 @@ impl Miner {
/// Uses Engine to seal the block internally and then imports it to chain. /// Uses Engine to seal the block internally and then imports it to chain.
fn seal_and_import_block_internally(&self, chain: &MiningBlockChainClient, block: ClosedBlock) -> bool { fn seal_and_import_block_internally(&self, chain: &MiningBlockChainClient, block: ClosedBlock) -> bool {
if !block.transactions().is_empty() { if !block.transactions().is_empty() || self.forced_sealing() {
if let Ok(sealed) = self.seal_block_internally(block) { if let Ok(sealed) = self.seal_block_internally(block) {
if chain.import_block(sealed.rlp_bytes()).is_ok() { if chain.import_block(sealed.rlp_bytes()).is_ok() {
trace!(target: "miner", "import_block_internally: imported internally sealed block");
return true return true
} }
} }
@ -773,7 +781,7 @@ impl MinerService for Miner {
chain: &MiningBlockChainClient, chain: &MiningBlockChainClient,
transactions: Vec<SignedTransaction> transactions: Vec<SignedTransaction>
) -> Vec<Result<TransactionImportResult, Error>> { ) -> Vec<Result<TransactionImportResult, Error>> {
trace!(target: "external_tx", "Importing external transactions");
let results = { let results = {
let mut transaction_queue = self.transaction_queue.lock(); let mut transaction_queue = self.transaction_queue.lock();
self.add_transactions_to_queue( self.add_transactions_to_queue(
@ -1251,7 +1259,7 @@ mod tests {
#[test] #[test]
fn internal_seals_without_work() { fn internal_seals_without_work() {
let miner = Miner::with_spec(&Spec::new_test_instant()); let miner = Miner::with_spec(&Spec::new_instant());
let c = generate_dummy_client(2); let c = generate_dummy_client(2);
let client = c.reference().as_ref(); let client = c.reference().as_ref();

View File

@ -48,6 +48,8 @@ pub enum ClientIoMessage {
FeedBlockChunk(H256, Bytes), FeedBlockChunk(H256, Bytes),
/// Take a snapshot for the block with given number. /// Take a snapshot for the block with given number.
TakeSnapshot(u64), TakeSnapshot(u64),
/// Trigger sealing update (useful for internal sealing).
UpdateSealing,
} }
/// Client service setup. Creates and registers client and network services with the IO subsystem. /// Client service setup. Creates and registers client and network services with the IO subsystem.
@ -111,6 +113,8 @@ impl ClientService {
}); });
try!(io_service.register_handler(client_io)); try!(io_service.register_handler(client_io));
spec.engine.register_message_channel(io_service.channel());
let stop_guard = ::devtools::StopGuard::new(); let stop_guard = ::devtools::StopGuard::new();
run_ipc(ipc_path, client.clone(), snapshot.clone(), stop_guard.share()); run_ipc(ipc_path, client.clone(), snapshot.clone(), stop_guard.share());
@ -213,8 +217,11 @@ impl IoHandler<ClientIoMessage> for ClientIoHandler {
if let Err(e) = res { if let Err(e) = res {
debug!(target: "snapshot", "Failed to initialize periodic snapshot thread: {:?}", e); debug!(target: "snapshot", "Failed to initialize periodic snapshot thread: {:?}", e);
} }
},
} ClientIoMessage::UpdateSealing => {
trace!(target: "authorityround", "message: UpdateSealing");
self.client.update_sealing()
},
_ => {} // ignore other messages _ => {} // ignore other messages
} }
} }

View File

@ -19,11 +19,11 @@
use account_db::{AccountDB, AccountDBMut}; use account_db::{AccountDB, AccountDBMut};
use snapshot::Error; use snapshot::Error;
use util::{U256, FixedHash, H256, Bytes, HashDB, DBValue, SHA3_EMPTY, SHA3_NULL_RLP}; use util::{U256, FixedHash, H256, Bytes, HashDB, SHA3_EMPTY, SHA3_NULL_RLP};
use util::trie::{TrieDB, Trie}; use util::trie::{TrieDB, Trie};
use rlp::{Rlp, RlpStream, Stream, UntrustedRlp, View}; use rlp::{Rlp, RlpStream, Stream, UntrustedRlp, View};
use std::collections::{HashMap, HashSet}; use std::collections::HashSet;
// An empty account -- these are replaced with RLP null data for a space optimization. // An empty account -- these are replaced with RLP null data for a space optimization.
const ACC_EMPTY: Account = Account { const ACC_EMPTY: Account = Account {
@ -150,7 +150,6 @@ impl Account {
pub fn from_fat_rlp( pub fn from_fat_rlp(
acct_db: &mut AccountDBMut, acct_db: &mut AccountDBMut,
rlp: UntrustedRlp, rlp: UntrustedRlp,
code_map: &HashMap<H256, Bytes>,
) -> Result<(Self, Option<Bytes>), Error> { ) -> Result<(Self, Option<Bytes>), Error> {
use util::{TrieDBMut, TrieMut}; use util::{TrieDBMut, TrieMut};
@ -177,9 +176,6 @@ impl Account {
} }
CodeState::Hash => { CodeState::Hash => {
let code_hash = try!(rlp.val_at(3)); let code_hash = try!(rlp.val_at(3));
if let Some(code) = code_map.get(&code_hash) {
acct_db.emplace(code_hash.clone(), DBValue::from_slice(code));
}
(code_hash, None) (code_hash, None)
} }
@ -229,7 +225,7 @@ mod tests {
use util::{Address, FixedHash, H256, HashDB, DBValue}; use util::{Address, FixedHash, H256, HashDB, DBValue};
use rlp::{UntrustedRlp, View}; use rlp::{UntrustedRlp, View};
use std::collections::{HashSet, HashMap}; use std::collections::HashSet;
use super::{ACC_EMPTY, Account}; use super::{ACC_EMPTY, Account};
@ -250,7 +246,7 @@ mod tests {
let fat_rlp = account.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &addr), &mut Default::default()).unwrap(); let fat_rlp = account.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &addr), &mut Default::default()).unwrap();
let fat_rlp = UntrustedRlp::new(&fat_rlp); let fat_rlp = UntrustedRlp::new(&fat_rlp);
assert_eq!(Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr), fat_rlp, &Default::default()).unwrap().0, account); assert_eq!(Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr), fat_rlp).unwrap().0, account);
} }
#[test] #[test]
@ -275,7 +271,7 @@ mod tests {
let fat_rlp = account.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &addr), &mut Default::default()).unwrap(); let fat_rlp = account.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &addr), &mut Default::default()).unwrap();
let fat_rlp = UntrustedRlp::new(&fat_rlp); let fat_rlp = UntrustedRlp::new(&fat_rlp);
assert_eq!(Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr), fat_rlp, &Default::default()).unwrap().0, account); assert_eq!(Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr), fat_rlp).unwrap().0, account);
} }
#[test] #[test]
@ -318,12 +314,11 @@ mod tests {
let fat_rlp1 = UntrustedRlp::new(&fat_rlp1); let fat_rlp1 = UntrustedRlp::new(&fat_rlp1);
let fat_rlp2 = UntrustedRlp::new(&fat_rlp2); let fat_rlp2 = UntrustedRlp::new(&fat_rlp2);
let code_map = HashMap::new(); let (acc, maybe_code) = Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr2), fat_rlp2).unwrap();
let (acc, maybe_code) = Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr2), fat_rlp2, &code_map).unwrap();
assert!(maybe_code.is_none()); assert!(maybe_code.is_none());
assert_eq!(acc, account2); assert_eq!(acc, account2);
let (acc, maybe_code) = Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr1), fat_rlp1, &code_map).unwrap(); let (acc, maybe_code) = Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &addr1), fat_rlp1).unwrap();
assert_eq!(maybe_code, Some(b"this is definitely code".to_vec())); assert_eq!(maybe_code, Some(b"this is definitely code".to_vec()));
assert_eq!(acc, account1); assert_eq!(acc, account1);
} }
@ -332,9 +327,8 @@ mod tests {
fn encoding_empty_acc() { fn encoding_empty_acc() {
let mut db = get_temp_state_db(); let mut db = get_temp_state_db();
let mut used_code = HashSet::new(); let mut used_code = HashSet::new();
let code_map = HashMap::new();
assert_eq!(ACC_EMPTY.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &Address::default()), &mut used_code).unwrap(), ::rlp::NULL_RLP.to_vec()); assert_eq!(ACC_EMPTY.to_fat_rlp(&AccountDB::new(db.as_hashdb(), &Address::default()), &mut used_code).unwrap(), ::rlp::NULL_RLP.to_vec());
assert_eq!(Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &Address::default()), UntrustedRlp::new(&::rlp::NULL_RLP), &code_map).unwrap(), (ACC_EMPTY, None)); assert_eq!(Account::from_fat_rlp(&mut AccountDBMut::new(db.as_hashdb_mut(), &Address::default()), UntrustedRlp::new(&::rlp::NULL_RLP)).unwrap(), (ACC_EMPTY, None));
} }
} }

View File

@ -389,7 +389,7 @@ pub fn chunk_state<'a>(db: &HashDB, root: &H256, writer: &Mutex<SnapshotWriter +
pub struct StateRebuilder { pub struct StateRebuilder {
db: Box<JournalDB>, db: Box<JournalDB>,
state_root: H256, state_root: H256,
code_map: HashMap<H256, Bytes>, // maps code hashes to code itself. known_code: HashMap<H256, H256>, // code hashes mapped to first account with this code.
missing_code: HashMap<H256, Vec<H256>>, // maps code hashes to lists of accounts missing that code. missing_code: HashMap<H256, Vec<H256>>, // maps code hashes to lists of accounts missing that code.
bloom: Bloom, bloom: Bloom,
} }
@ -400,7 +400,7 @@ impl StateRebuilder {
StateRebuilder { StateRebuilder {
db: journaldb::new(db.clone(), pruning, ::db::COL_STATE), db: journaldb::new(db.clone(), pruning, ::db::COL_STATE),
state_root: SHA3_NULL_RLP, state_root: SHA3_NULL_RLP,
code_map: HashMap::new(), known_code: HashMap::new(),
missing_code: HashMap::new(), missing_code: HashMap::new(),
bloom: StateDB::load_bloom(&*db), bloom: StateDB::load_bloom(&*db),
} }
@ -419,7 +419,7 @@ impl StateRebuilder {
self.db.as_hashdb_mut(), self.db.as_hashdb_mut(),
rlp, rlp,
&mut pairs, &mut pairs,
&self.code_map, &self.known_code,
flag flag
)); ));
@ -428,13 +428,13 @@ impl StateRebuilder {
} }
// patch up all missing code. must be done after collecting all new missing code entries. // patch up all missing code. must be done after collecting all new missing code entries.
for (code_hash, code) in status.new_code { for (code_hash, code, first_with) in status.new_code {
for addr_hash in self.missing_code.remove(&code_hash).unwrap_or_else(Vec::new) { for addr_hash in self.missing_code.remove(&code_hash).unwrap_or_else(Vec::new) {
let mut db = AccountDBMut::from_hash(self.db.as_hashdb_mut(), addr_hash); let mut db = AccountDBMut::from_hash(self.db.as_hashdb_mut(), addr_hash);
db.emplace(code_hash, DBValue::from_slice(&code)); db.emplace(code_hash, DBValue::from_slice(&code));
} }
self.code_map.insert(code_hash, code); self.known_code.insert(code_hash, first_with);
} }
let backing = self.db.backing().clone(); let backing = self.db.backing().clone();
@ -482,7 +482,8 @@ impl StateRebuilder {
#[derive(Default)] #[derive(Default)]
struct RebuiltStatus { struct RebuiltStatus {
new_code: Vec<(H256, Bytes)>, // new code that's become available. // new code that's become available. (code_hash, code, addr_hash)
new_code: Vec<(H256, Bytes, H256)>,
missing_code: Vec<(H256, H256)>, // accounts that are missing code. missing_code: Vec<(H256, H256)>, // accounts that are missing code.
} }
@ -492,10 +493,9 @@ fn rebuild_accounts(
db: &mut HashDB, db: &mut HashDB,
account_fat_rlps: UntrustedRlp, account_fat_rlps: UntrustedRlp,
out_chunk: &mut [(H256, Bytes)], out_chunk: &mut [(H256, Bytes)],
code_map: &HashMap<H256, Bytes>, known_code: &HashMap<H256, H256>,
abort_flag: &AtomicBool abort_flag: &AtomicBool,
) -> Result<RebuiltStatus, ::error::Error> ) -> Result<RebuiltStatus, ::error::Error> {
{
let mut status = RebuiltStatus::default(); let mut status = RebuiltStatus::default();
for (account_rlp, out) in account_fat_rlps.into_iter().zip(out_chunk) { for (account_rlp, out) in account_fat_rlps.into_iter().zip(out_chunk) {
if !abort_flag.load(Ordering::SeqCst) { return Err(Error::RestorationAborted.into()) } if !abort_flag.load(Ordering::SeqCst) { return Err(Error::RestorationAborted.into()) }
@ -504,17 +504,33 @@ fn rebuild_accounts(
let fat_rlp = try!(account_rlp.at(1)); let fat_rlp = try!(account_rlp.at(1));
let thin_rlp = { let thin_rlp = {
let mut acct_db = AccountDBMut::from_hash(db, hash);
// fill out the storage trie and code while decoding. // fill out the storage trie and code while decoding.
let (acc, maybe_code) = try!(Account::from_fat_rlp(&mut acct_db, fat_rlp, code_map)); let (acc, maybe_code) = {
let mut acct_db = AccountDBMut::from_hash(db, hash);
try!(Account::from_fat_rlp(&mut acct_db, fat_rlp))
};
let code_hash = acc.code_hash().clone(); let code_hash = acc.code_hash().clone();
match maybe_code { match maybe_code {
Some(code) => status.new_code.push((code_hash, code)), // new inline code
Some(code) => status.new_code.push((code_hash, code, hash)),
None => { None => {
if code_hash != ::util::SHA3_EMPTY && !code_map.contains_key(&code_hash) { if code_hash != ::util::SHA3_EMPTY {
status.missing_code.push((hash, code_hash)); // see if this code has already been included inline
match known_code.get(&code_hash) {
Some(&first_with) => {
// if so, load it from the database.
let code = try!(AccountDB::from_hash(db, first_with)
.get(&code_hash)
.ok_or_else(|| Error::MissingCode(vec![first_with])));
// and write it again under a different mangled key
AccountDBMut::from_hash(db, hash).emplace(code_hash, code);
}
// if not, queue it up to be filled later
None => status.missing_code.push((hash, code_hash)),
}
} }
} }
} }

View File

@ -17,6 +17,7 @@
//! State snapshotting tests. //! State snapshotting tests.
use snapshot::{chunk_state, Error as SnapshotError, Progress, StateRebuilder}; use snapshot::{chunk_state, Error as SnapshotError, Progress, StateRebuilder};
use snapshot::account::Account;
use snapshot::io::{PackedReader, PackedWriter, SnapshotReader, SnapshotWriter}; use snapshot::io::{PackedReader, PackedWriter, SnapshotReader, SnapshotWriter};
use super::helpers::{compare_dbs, StateProducer}; use super::helpers::{compare_dbs, StateProducer};
@ -30,6 +31,8 @@ use util::memorydb::MemoryDB;
use util::Mutex; use util::Mutex;
use devtools::RandomTempPath; use devtools::RandomTempPath;
use util::sha3::SHA3_NULL_RLP;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
@ -88,6 +91,58 @@ fn snap_and_restore() {
compare_dbs(&old_db, new_db.as_hashdb()); compare_dbs(&old_db, new_db.as_hashdb());
} }
#[test]
fn get_code_from_prev_chunk() {
use std::collections::HashSet;
use rlp::{RlpStream, Stream};
use util::{HashDB, H256, FixedHash, U256, Hashable};
use account_db::{AccountDBMut, AccountDB};
let code = b"this is definitely code";
let mut used_code = HashSet::new();
let mut acc_stream = RlpStream::new_list(4);
acc_stream.append(&U256::default())
.append(&U256::default())
.append(&SHA3_NULL_RLP)
.append(&code.sha3());
let (h1, h2) = (H256::random(), H256::random());
// two accounts with the same code, one per chunk.
// first one will have code inlined,
// second will just have its hash.
let thin_rlp = acc_stream.out();
let acc1 = Account::from_thin_rlp(&thin_rlp);
let acc2 = Account::from_thin_rlp(&thin_rlp);
let mut make_chunk = |acc: Account, hash| {
let mut db = MemoryDB::new();
AccountDBMut::from_hash(&mut db, hash).insert(&code[..]);
let fat_rlp = acc.to_fat_rlp(&AccountDB::from_hash(&db, hash), &mut used_code).unwrap();
let mut stream = RlpStream::new_list(1);
stream.begin_list(2).append(&hash).append_raw(&fat_rlp, 1);
stream.out()
};
let chunk1 = make_chunk(acc1, h1);
let chunk2 = make_chunk(acc2, h2);
let db_path = RandomTempPath::create_dir();
let db_cfg = DatabaseConfig::with_columns(::db::NUM_COLUMNS);
let new_db = Arc::new(Database::open(&db_cfg, &db_path.to_string_lossy()).unwrap());
let mut rebuilder = StateRebuilder::new(new_db, Algorithm::Archive);
let flag = AtomicBool::new(true);
rebuilder.feed(&chunk1, &flag).unwrap();
rebuilder.feed(&chunk2, &flag).unwrap();
rebuilder.check_missing().unwrap();
}
#[test] #[test]
fn checks_flag() { fn checks_flag() {
let mut producer = StateProducer::new(); let mut producer = StateProducer::new();

View File

@ -18,7 +18,7 @@
use util::*; use util::*;
use builtin::Builtin; use builtin::Builtin;
use engines::{Engine, NullEngine, InstantSeal, BasicAuthority}; use engines::{Engine, NullEngine, InstantSeal, BasicAuthority, AuthorityRound};
use pod_state::*; use pod_state::*;
use account_db::*; use account_db::*;
use header::{BlockNumber, Header}; use header::{BlockNumber, Header};
@ -135,6 +135,12 @@ impl From<ethjson::spec::Spec> for Spec {
} }
} }
macro_rules! load_bundled {
($e:expr) => {
Spec::load(include_bytes!(concat!("../../res/", $e, ".json")) as &[u8]).expect(concat!("Chain spec ", $e, " is invalid."))
};
}
impl Spec { impl Spec {
/// Convert engine spec into a arc'd Engine of the right underlying type. /// Convert engine spec into a arc'd Engine of the right underlying type.
/// TODO avoid this hard-coded nastiness - use dynamic-linked plugin framework instead. /// TODO avoid this hard-coded nastiness - use dynamic-linked plugin framework instead.
@ -144,6 +150,7 @@ impl Spec {
ethjson::spec::Engine::InstantSeal => Arc::new(InstantSeal::new(params, builtins)), ethjson::spec::Engine::InstantSeal => Arc::new(InstantSeal::new(params, builtins)),
ethjson::spec::Engine::Ethash(ethash) => Arc::new(ethereum::Ethash::new(params, From::from(ethash.params), builtins)), ethjson::spec::Engine::Ethash(ethash) => Arc::new(ethereum::Ethash::new(params, From::from(ethash.params), builtins)),
ethjson::spec::Engine::BasicAuthority(basic_authority) => Arc::new(BasicAuthority::new(params, From::from(basic_authority.params), builtins)), ethjson::spec::Engine::BasicAuthority(basic_authority) => Arc::new(BasicAuthority::new(params, From::from(basic_authority.params), builtins)),
ethjson::spec::Engine::AuthorityRound(authority_round) => AuthorityRound::new(params, From::from(authority_round.params), builtins).expect("Consensus engine could not be started."),
} }
} }
@ -267,19 +274,17 @@ impl Spec {
} }
/// Create a new Spec which conforms to the Frontier-era Morden chain except that it's a NullEngine consensus. /// Create a new Spec which conforms to the Frontier-era Morden chain except that it's a NullEngine consensus.
pub fn new_test() -> Self { pub fn new_test() -> Spec { load_bundled!("null_morden") }
Spec::load(include_bytes!("../../res/null_morden.json") as &[u8]).expect("null_morden.json is invalid")
}
/// Create a new Spec which is a NullEngine consensus with a premine of address whose secret is sha3(''). /// Create a new Spec which is a NullEngine consensus with a premine of address whose secret is sha3('').
pub fn new_null() -> Self { pub fn new_null() -> Spec { load_bundled!("null") }
Spec::load(include_bytes!("../../res/null.json") as &[u8]).expect("null.json is invalid")
}
/// Create a new Spec with InstantSeal consensus which does internal sealing (not requiring work). /// Create a new Spec with InstantSeal consensus which does internal sealing (not requiring work).
pub fn new_test_instant() -> Self { pub fn new_instant() -> Spec { load_bundled!("instant_seal") }
Spec::load(include_bytes!("../../res/instant_seal.json") as &[u8]).expect("instant_seal.json is invalid")
} /// Create a new Spec with AuthorityRound consensus which does internal sealing (not requiring work).
/// Accounts with secrets "1".sha3() and "2".sha3() are the authorities.
pub fn new_test_round() -> Self { load_bundled!("authority_round") }
} }
#[cfg(test)] #[cfg(test)]

View File

@ -433,18 +433,19 @@ pub fn get_default_ethash_params() -> EthashParams{
block_reward: U256::from(0), block_reward: U256::from(0),
registrar: "0000000000000000000000000000000000000001".into(), registrar: "0000000000000000000000000000000000000001".into(),
homestead_transition: 1150000, homestead_transition: 1150000,
dao_hardfork_transition: 0x7fffffffffffffff, dao_hardfork_transition: u64::max_value(),
dao_hardfork_beneficiary: "0000000000000000000000000000000000000001".into(), dao_hardfork_beneficiary: "0000000000000000000000000000000000000001".into(),
dao_hardfork_accounts: vec![], dao_hardfork_accounts: vec![],
difficulty_hardfork_transition: 0x7fffffffffffffff, difficulty_hardfork_transition: u64::max_value(),
difficulty_hardfork_bound_divisor: U256::from(0), difficulty_hardfork_bound_divisor: U256::from(0),
bomb_defuse_transition: 0x7fffffffffffffff, bomb_defuse_transition: u64::max_value(),
eip150_transition: 0x7fffffffffffffff, eip150_transition: u64::max_value(),
eip155_transition: 0x7fffffffffffffff, eip155_transition: u64::max_value(),
eip160_transition: 0x7fffffffffffffff, eip160_transition: u64::max_value(),
eip161abc_transition: 0x7fffffffffffffff, eip161abc_transition: u64::max_value(),
eip161d_transition: 0x7fffffffffffffff, eip161d_transition: u64::max_value(),
ecip1010_pause_transition: 0x7fffffffffffffff, ecip1010_pause_transition: u64::max_value(),
ecip1010_continue_transition: 0x7fffffffffffffff ecip1010_continue_transition: u64::max_value(),
max_code_size: u64::max_value(),
} }
} }

View File

@ -34,16 +34,43 @@ pub const KEY_LENGTH: usize = 32;
pub const KEY_ITERATIONS: usize = 10240; pub const KEY_ITERATIONS: usize = 10240;
pub const KEY_LENGTH_AES: usize = KEY_LENGTH / 2; pub const KEY_LENGTH_AES: usize = KEY_LENGTH / 2;
#[derive(PartialEq, Debug)]
pub enum ScryptError {
// log(N) < r / 16
InvalidN,
// p <= (2^31-1 * 32)/(128 * r)
InvalidP,
}
impl fmt::Display for ScryptError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
let s = match *self {
ScryptError::InvalidN => "Invalid N argument of the scrypt encryption" ,
ScryptError::InvalidP => "Invalid p argument of the scrypt encryption",
};
write!(f, "{}", s)
}
}
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub enum Error { pub enum Error {
Secp(SecpError), Secp(SecpError),
Scrypt(ScryptError),
InvalidMessage, InvalidMessage,
} }
impl From<ScryptError> for Error {
fn from(err: ScryptError) -> Self {
Error::Scrypt(err)
}
}
impl fmt::Display for Error { impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
let s = match *self { let s = match *self {
Error::Secp(ref err) => err.to_string(), Error::Secp(ref err) => err.to_string(),
Error::Scrypt(ref err) => err.to_string(),
Error::InvalidMessage => "Invalid message".into(), Error::InvalidMessage => "Invalid message".into(),
}; };
@ -80,13 +107,23 @@ pub fn derive_key_iterations(password: &str, salt: &[u8; 32], c: u32) -> (Vec<u8
(derived_right_bits.to_vec(), derived_left_bits.to_vec()) (derived_right_bits.to_vec(), derived_left_bits.to_vec())
} }
pub fn derive_key_scrypt(password: &str, salt: &[u8; 32], n: u32, p: u32, r: u32) -> (Vec<u8>, Vec<u8>) { pub fn derive_key_scrypt(password: &str, salt: &[u8; 32], n: u32, p: u32, r: u32) -> Result<(Vec<u8>, Vec<u8>), Error> {
// sanity checks
let log_n = (32 - n.leading_zeros() - 1) as u8;
if log_n as u32 >= r * 16 {
return Err(Error::Scrypt(ScryptError::InvalidN));
}
if p as u64 > ((u32::max_value() as u64 - 1) * 32)/(128 * (r as u64)) {
return Err(Error::Scrypt(ScryptError::InvalidP));
}
let mut derived_key = vec![0u8; KEY_LENGTH]; let mut derived_key = vec![0u8; KEY_LENGTH];
let scrypt_params = ScryptParams::new(n.trailing_zeros() as u8, r, p); let scrypt_params = ScryptParams::new(log_n, r, p);
scrypt(password.as_bytes(), salt, &scrypt_params, &mut derived_key); scrypt(password.as_bytes(), salt, &scrypt_params, &mut derived_key);
let derived_right_bits = &derived_key[0..KEY_LENGTH_AES]; let derived_right_bits = &derived_key[0..KEY_LENGTH_AES];
let derived_left_bits = &derived_key[KEY_LENGTH_AES..KEY_LENGTH]; let derived_left_bits = &derived_key[KEY_LENGTH_AES..KEY_LENGTH];
(derived_right_bits.to_vec(), derived_left_bits.to_vec()) Ok((derived_right_bits.to_vec(), derived_left_bits.to_vec()))
} }
pub fn derive_mac(derived_left_bits: &[u8], cipher_text: &[u8]) -> Vec<u8> { pub fn derive_mac(derived_left_bits: &[u8], cipher_text: &[u8]) -> Vec<u8> {

File diff suppressed because it is too large Load Diff

View File

@ -113,7 +113,7 @@ impl Crypto {
let (derived_left_bits, derived_right_bits) = match self.kdf { let (derived_left_bits, derived_right_bits) = match self.kdf {
Kdf::Pbkdf2(ref params) => crypto::derive_key_iterations(password, &params.salt, params.c), Kdf::Pbkdf2(ref params) => crypto::derive_key_iterations(password, &params.salt, params.c),
Kdf::Scrypt(ref params) => crypto::derive_key_scrypt(password, &params.salt, params.n, params.p, params.r), Kdf::Scrypt(ref params) => try!(crypto::derive_key_scrypt(password, &params.salt, params.n, params.p, params.r)),
}; };
let mac = crypto::derive_mac(&derived_right_bits, &self.ciphertext).keccak256(); let mac = crypto::derive_mac(&derived_right_bits, &self.ciphertext).keccak256();

View File

@ -20,6 +20,7 @@ use std::collections::HashMap;
use time; use time;
use ethkey::Address; use ethkey::Address;
use {json, SafeAccount, Error}; use {json, SafeAccount, Error};
use json::UUID;
use super::KeyDirectory; use super::KeyDirectory;
const IGNORED_FILES: &'static [&'static str] = &["thumbs.db", "address_book.json"]; const IGNORED_FILES: &'static [&'static str] = &["thumbs.db", "address_book.json"];
@ -112,7 +113,7 @@ impl KeyDirectory for DiskDirectory {
// build file path // build file path
let filename = account.filename.as_ref().cloned().unwrap_or_else(|| { let filename = account.filename.as_ref().cloned().unwrap_or_else(|| {
let timestamp = time::strftime("%Y-%m-%dT%H-%M-%S", &time::now_utc()).expect("Time-format string is valid."); let timestamp = time::strftime("%Y-%m-%dT%H-%M-%S", &time::now_utc()).expect("Time-format string is valid.");
format!("UTC--{}Z--{:?}", timestamp, account.address) format!("UTC--{}Z--{}", timestamp, UUID::from(account.id))
}); });
// update account filename // update account filename

View File

@ -1,6 +1,6 @@
{ {
"name": "parity.js", "name": "parity.js",
"version": "0.2.35", "version": "0.2.52",
"main": "release/index.js", "main": "release/index.js",
"jsnext:main": "src/index.js", "jsnext:main": "src/index.js",
"author": "Parity Team <admin@parity.io>", "author": "Parity Team <admin@parity.io>",
@ -122,6 +122,7 @@
"brace": "^0.9.0", "brace": "^0.9.0",
"bytes": "^2.4.0", "bytes": "^2.4.0",
"chart.js": "^2.3.0", "chart.js": "^2.3.0",
"es6-error": "^4.0.0",
"es6-promise": "^3.2.1", "es6-promise": "^3.2.1",
"ethereumjs-tx": "^1.1.2", "ethereumjs-tx": "^1.1.2",
"file-saver": "^1.3.3", "file-saver": "^1.3.3",
@ -138,6 +139,7 @@
"mobx-react": "^3.5.8", "mobx-react": "^3.5.8",
"mobx-react-devtools": "^4.2.9", "mobx-react-devtools": "^4.2.9",
"moment": "^2.14.1", "moment": "^2.14.1",
"phoneformat.js": "^1.0.3",
"qs": "^6.3.0", "qs": "^6.3.0",
"react": "^15.2.1", "react": "^15.2.1",
"react-ace": "^4.0.0", "react-ace": "^4.0.0",

View File

@ -68,11 +68,13 @@ if [ "$BRANCH" == "master" ]; then
fi fi
echo "*** Updating cargo parity-ui-precompiled#$PRECOMPILED_HASH" echo "*** Updating cargo parity-ui-precompiled#$PRECOMPILED_HASH"
git submodule update
cargo update -p parity-ui-precompiled cargo update -p parity-ui-precompiled
# --precise "$PRECOMPILED_HASH" # --precise "$PRECOMPILED_HASH"
echo "*** Committing updated files" echo "*** Committing updated files"
git add . git add js
git add Cargo.lock
git commit -m "[ci skip] js-precompiled $UTCDATE" git commit -m "[ci skip] js-precompiled $UTCDATE"
git push origin HEAD:refs/heads/$BRANCH 2>$GITLOG git push origin HEAD:refs/heads/$BRANCH 2>$GITLOG

View File

@ -46,7 +46,8 @@ function stringToBytes (input) {
if (isArray(input)) { if (isArray(input)) {
return input; return input;
} else if (input.substr(0, 2) === '0x') { } else if (input.substr(0, 2) === '0x') {
return input.substr(2).toLowerCase().match(/.{1,2}/g).map((value) => parseInt(value, 16)); const matches = input.substr(2).toLowerCase().match(/.{1,2}/g) || [];
return matches.map((value) => parseInt(value, 16));
} else { } else {
return input.split('').map((char) => char.charCodeAt(0)); return input.split('').map((char) => char.charCodeAt(0));
} }

View File

@ -40,9 +40,12 @@ export default class Contract {
this._events.forEach((evt) => { this._events.forEach((evt) => {
this._instance[evt.name] = evt; this._instance[evt.name] = evt;
this._instance[evt.signature] = evt;
}); });
this._functions.forEach((fn) => { this._functions.forEach((fn) => {
this._instance[fn.name] = fn; this._instance[fn.name] = fn;
this._instance[fn.signature] = fn;
}); });
this._sendSubscriptionChanges(); this._sendSubscriptionChanges();

View File

@ -20,6 +20,7 @@ import sinon from 'sinon';
import { TEST_HTTP_URL, mockHttp } from '../../../test/mockRpc'; import { TEST_HTTP_URL, mockHttp } from '../../../test/mockRpc';
import Abi from '../../abi'; import Abi from '../../abi';
import { sha3 } from '../util/sha3';
import Api from '../api'; import Api from '../api';
import Contract from './contract'; import Contract from './contract';
@ -113,7 +114,13 @@ describe('api/contract/Contract', () => {
]); ]);
contract.at('6789'); contract.at('6789');
expect(Object.keys(contract.instance)).to.deep.equal(['Drained', 'balanceOf', 'address']); expect(Object.keys(contract.instance)).to.deep.equal([
'Drained',
/^(?:0x)(.+)$/.exec(sha3('Drained(uint256)'))[1],
'balanceOf',
/^(?:0x)(.+)$/.exec(sha3('balanceOf(address)'))[1].substr(0, 8),
'address'
]);
expect(contract.address).to.equal('6789'); expect(contract.address).to.equal('6789');
}); });
}); });

View File

@ -166,3 +166,11 @@ export function inTraceFilter (filterObject) {
return filterObject; return filterObject;
} }
export function inTraceType (whatTrace) {
if (isString(whatTrace)) {
return [whatTrace];
}
return whatTrace;
}

View File

@ -16,7 +16,7 @@
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { inAddress, inBlockNumber, inData, inFilter, inHex, inNumber10, inNumber16, inOptions } from './input'; import { inAddress, inBlockNumber, inData, inFilter, inHex, inNumber10, inNumber16, inOptions, inTraceType } from './input';
import { isAddress } from '../../../test/types'; import { isAddress } from '../../../test/types';
describe('api/format/input', () => { describe('api/format/input', () => {
@ -242,4 +242,16 @@ describe('api/format/input', () => {
}); });
}); });
}); });
describe('inTraceType', () => {
it('returns array of types as is', () => {
const types = ['vmTrace', 'trace', 'stateDiff'];
expect(inTraceType(types)).to.deep.equal(types);
});
it('formats single string type into array', () => {
const type = 'vmTrace';
expect(inTraceType(type)).to.deep.equal([type]);
});
});
}); });

View File

@ -254,3 +254,25 @@ export function outTrace (trace) {
return trace; return trace;
} }
export function outTraces (traces) {
if (traces) {
return traces.map(outTrace);
}
return traces;
}
export function outTraceReplay (trace) {
if (trace) {
Object.keys(trace).forEach((key) => {
switch (key) {
case 'trace':
trace[key] = outTraces(trace[key]);
break;
}
});
}
return trace;
}

View File

@ -20,15 +20,25 @@ describe('ethapi.trace', () => {
const ethapi = createHttpApi(); const ethapi = createHttpApi();
describe('block', () => { describe('block', () => {
it('returns the latest block', () => { it('returns the latest block traces', () => {
return ethapi.trace.block().then((block) => { return ethapi.trace.block().then((traces) => {
expect(block).to.be.ok; expect(traces).to.be.ok;
}); });
}); });
it('returns a specified block', () => { it('returns traces for a specified block', () => {
return ethapi.trace.block('0x65432').then((block) => { return ethapi.trace.block('0x65432').then((traces) => {
expect(block).to.be.ok; expect(traces).to.be.ok;
});
});
});
describe('replayTransaction', () => {
it('returns traces for a specific transaction', () => {
return ethapi.eth.getBlockByNumber().then((latestBlock) => {
return ethapi.trace.replayTransaction(latestBlock.transactions[0]).then((traces) => {
expect(traces).to.be.ok;
});
}); });
}); });
}); });

View File

@ -14,35 +14,53 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { inBlockNumber, inHex, inNumber16, inTraceFilter } from '../../format/input'; import { inBlockNumber, inData, inHex, inNumber16, inOptions, inTraceFilter, inTraceType } from '../../format/input';
import { outTrace } from '../../format/output'; import { outTraces, outTraceReplay } from '../../format/output';
export default class Trace { export default class Trace {
constructor (transport) { constructor (transport) {
this._transport = transport; this._transport = transport;
} }
block (blockNumber = 'latest') {
return this._transport
.execute('trace_block', inBlockNumber(blockNumber))
.then(outTraces);
}
call (options, blockNumber = 'latest', whatTrace = ['trace']) {
return this._transport
.execute('trace_call', inOptions(options), inBlockNumber(blockNumber), inTraceType(whatTrace))
.then(outTraceReplay);
}
filter (filterObj) { filter (filterObj) {
return this._transport return this._transport
.execute('trace_filter', inTraceFilter(filterObj)) .execute('trace_filter', inTraceFilter(filterObj))
.then(traces => traces.map(trace => outTrace(trace))); .then(outTraces);
} }
get (txHash, position) { get (txHash, position) {
return this._transport return this._transport
.execute('trace_get', inHex(txHash), inNumber16(position)) .execute('trace_get', inHex(txHash), inNumber16(position))
.then(trace => outTrace(trace)); .then(outTraces);
}
rawTransaction (data, whatTrace = ['trace']) {
return this._transport
.execute('trace_rawTransaction', inData(data), inTraceType(whatTrace))
.then(outTraceReplay);
}
replayTransaction (txHash, whatTrace = ['trace']) {
return this._transport
.execute('trace_replayTransaction', txHash, inTraceType(whatTrace))
.then(outTraceReplay);
} }
transaction (txHash) { transaction (txHash) {
return this._transport return this._transport
.execute('trace_transaction', inHex(txHash)) .execute('trace_transaction', inHex(txHash))
.then(traces => traces.map(trace => outTrace(trace))); .then(outTraces);
}
block (blockNumber = 'latest') {
return this._transport
.execute('trace_block', inBlockNumber(blockNumber))
.then(traces => traces.map(trace => outTrace(trace)));
} }
} }

View File

@ -0,0 +1,53 @@
// Copyright 2015, 2016 Ethcore (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 ExtendableError from 'es6-error';
export const ERROR_CODES = {
UNSUPPORTED_REQUEST: -32000,
NO_WORK: -32001,
NO_AUTHOR: -32002,
NO_NEW_WORK: -32003,
NOT_ENOUGH_DATA: -32006,
UNKNOWN_ERROR: -32009,
TRANSACTION_ERROR: -32010,
EXECUTION_ERROR: -32015,
ACCOUNT_LOCKED: -32020,
PASSWORD_INVALID: -32021,
ACCOUNT_ERROR: -32023,
SIGNER_DISABLED: -32030,
DAPPS_DISABLED: -32031,
NETWORK_DISABLED: -32035,
REQUEST_REJECTED: -32040,
REQUEST_REJECTED_LIMIT: -32041,
REQUEST_NOT_FOUND: -32042,
COMPILATION_ERROR: -32050,
ENCRYPTION_ERROR: -32055,
FETCH_ERROR: -32060
};
export default class TransportError extends ExtendableError {
constructor (method, code, message) {
const m = `${method}: ${code}: ${message}`;
super(m);
this.code = code;
this.type = Object.keys(ERROR_CODES).find((k) => ERROR_CODES[k] === code) || '';
this.method = method;
this.text = message;
}
}

View File

@ -16,6 +16,7 @@
import { Logging } from '../../subscriptions'; import { Logging } from '../../subscriptions';
import JsonRpcBase from '../jsonRpcBase'; import JsonRpcBase from '../jsonRpcBase';
import TransportError from '../error';
/* global fetch */ /* global fetch */
export default class Http extends JsonRpcBase { export default class Http extends JsonRpcBase {
@ -73,7 +74,8 @@ export default class Http extends JsonRpcBase {
this.error(JSON.stringify(response)); this.error(JSON.stringify(response));
console.error(`${method}(${JSON.stringify(params)}): ${response.error.code}: ${response.error.message}`); console.error(`${method}(${JSON.stringify(params)}): ${response.error.code}: ${response.error.message}`);
throw new Error(`${method}: ${response.error.code}: ${response.error.message}`); const error = new TransportError(method, response.error.code, response.error.message);
throw error;
} }
this.log(JSON.stringify(response)); this.log(JSON.stringify(response));

View File

@ -16,3 +16,4 @@
export Http from './http'; export Http from './http';
export Ws from './ws'; export Ws from './ws';
export TransportError from './error.js';

View File

@ -18,6 +18,7 @@ import { keccak_256 } from 'js-sha3'; // eslint-disable-line camelcase
import { Logging } from '../../subscriptions'; import { Logging } from '../../subscriptions';
import JsonRpcBase from '../jsonRpcBase'; import JsonRpcBase from '../jsonRpcBase';
import TransportError from '../error';
/* global WebSocket */ /* global WebSocket */
export default class Ws extends JsonRpcBase { export default class Ws extends JsonRpcBase {
@ -109,7 +110,9 @@ export default class Ws extends JsonRpcBase {
console.error(`${method}(${JSON.stringify(params)}): ${result.error.code}: ${result.error.message}`); console.error(`${method}(${JSON.stringify(params)}): ${result.error.code}: ${result.error.message}`);
reject(new Error(`${method}: ${result.error.code}: ${result.error.message}`)); const error = new TransportError(method, result.error.code, result.error.message);
reject(error);
delete this._messages[result.id]; delete this._messages[result.id];
return; return;
} }

View File

@ -17,3 +17,15 @@
export function bytesToHex (bytes) { export function bytesToHex (bytes) {
return '0x' + bytes.map((b) => ('0' + b.toString(16)).slice(-2)).join(''); return '0x' + bytes.map((b) => ('0' + b.toString(16)).slice(-2)).join('');
} }
export function hex2Ascii (_hex) {
const hex = /^(?:0x)?(.*)$/.exec(_hex.toString())[1];
let str = '';
for (let i = 0; i < hex.length; i += 2) {
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
}
return str;
}

View File

@ -16,7 +16,7 @@
import { isAddress as isAddressValid, toChecksumAddress } from '../../abi/util/address'; import { isAddress as isAddressValid, toChecksumAddress } from '../../abi/util/address';
import { decodeCallData, decodeMethodInput, methodToAbi } from './decode'; import { decodeCallData, decodeMethodInput, methodToAbi } from './decode';
import { bytesToHex } from './format'; import { bytesToHex, hex2Ascii } from './format';
import { fromWei, toWei } from './wei'; import { fromWei, toWei } from './wei';
import { sha3 } from './sha3'; import { sha3 } from './sha3';
import { isArray, isFunction, isHex, isInstanceOf, isString } from './types'; import { isArray, isFunction, isHex, isInstanceOf, isString } from './types';
@ -30,6 +30,7 @@ export default {
isInstanceOf, isInstanceOf,
isString, isString,
bytesToHex, bytesToHex,
hex2Ascii,
createIdentityImg, createIdentityImg,
decodeCallData, decodeCallData,
decodeMethodInput, decodeMethodInput,

View File

@ -23,6 +23,7 @@ import githubhint from './githubhint.json';
import owned from './owned.json'; import owned from './owned.json';
import registry from './registry.json'; import registry from './registry.json';
import signaturereg from './signaturereg.json'; import signaturereg from './signaturereg.json';
import smsverification from './sms-verification.json';
import tokenreg from './tokenreg.json'; import tokenreg from './tokenreg.json';
import wallet from './wallet.json'; import wallet from './wallet.json';
@ -36,6 +37,7 @@ export {
owned, owned,
registry, registry,
signaturereg, signaturereg,
smsverification,
tokenreg, tokenreg,
wallet wallet
}; };

View File

@ -0,0 +1 @@
[{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"}],"name":"certify","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"request","outputs":[],"payable":true,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"},{"name":"_puzzle","type":"bytes32"}],"name":"puzzle","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getAddress","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_new","type":"uint256"}],"name":"setFee","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"}],"name":"revoke","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_code","type":"bytes32"}],"name":"confirm","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"drain","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"delegate","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getUint","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setDelegate","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"}],"name":"certified","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"get","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Requested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"},{"indexed":false,"name":"puzzle","type":"bytes32"}],"name":"Puzzled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Confirmed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Revoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"old","type":"address"},{"indexed":true,"name":"current","type":"address"}],"name":"NewOwner","type":"event"}]

View File

@ -18,6 +18,8 @@ import DappReg from './dappreg';
import Registry from './registry'; import Registry from './registry';
import SignatureReg from './signaturereg'; import SignatureReg from './signaturereg';
import TokenReg from './tokenreg'; import TokenReg from './tokenreg';
import GithubHint from './githubhint';
import smsVerification from './sms-verification';
let instance = null; let instance = null;
@ -30,6 +32,7 @@ export default class Contracts {
this._dappreg = new DappReg(api, this._registry); this._dappreg = new DappReg(api, this._registry);
this._signaturereg = new SignatureReg(api, this._registry); this._signaturereg = new SignatureReg(api, this._registry);
this._tokenreg = new TokenReg(api, this._registry); this._tokenreg = new TokenReg(api, this._registry);
this._githubhint = new GithubHint(api, this._registry);
} }
get registry () { get registry () {
@ -48,6 +51,14 @@ export default class Contracts {
return this._tokenreg; return this._tokenreg;
} }
get githubHint () {
return this._githubhint;
}
get smsVerification () {
return smsVerification;
}
static create (api) { static create (api) {
return new Contracts(api); return new Contracts(api);
} }

View File

@ -0,0 +1,32 @@
// Copyright 2015, 2016 Ethcore (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 class GithubHint {
constructor (api, registry) {
this._api = api;
this._registry = registry;
this.getInstance();
}
getContract () {
return this._registry.getContract('githubhint');
}
getInstance () {
return this.getContract().instance;
}
}

View File

@ -42,7 +42,7 @@ export default class Registry {
}); });
} }
getContractInstance (_name) { getContract (_name) {
const name = _name.toLowerCase(); const name = _name.toLowerCase();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -54,13 +54,19 @@ export default class Registry {
this this
.lookupAddress(name) .lookupAddress(name)
.then((address) => { .then((address) => {
this._contracts[name] = this._api.newContract(abis[name], address).instance; this._contracts[name] = this._api.newContract(abis[name], address);
resolve(this._contracts[name]); resolve(this._contracts[name]);
}) })
.catch(reject); .catch(reject);
}); });
} }
getContractInstance (_name) {
return this
.getContract(_name)
.then((contract) => contract.instance);
}
lookupAddress (_name) { lookupAddress (_name) {
const name = _name.toLowerCase(); const name = _name.toLowerCase();
const sha3 = this._api.util.sha3(name); const sha3 = this._api.util.sha3(name);

View File

@ -0,0 +1,52 @@
// Copyright 2015, 2016 Ethcore (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 { stringify } from 'querystring';
export const checkIfVerified = (contract, account) => {
return contract.instance.certified.call({}, [account]);
};
export const checkIfRequested = (contract, account) => {
return new Promise((resolve, reject) => {
contract.subscribe('Requested', {
fromBlock: 0, toBlock: 'pending'
}, (err, logs) => {
if (err) {
return reject(err);
}
const e = logs.find((l) => {
return l.type === 'mined' && l.params.who && l.params.who.value === account;
});
resolve(e ? e.transactionHash : false);
});
});
};
export const postToServer = (query) => {
query = stringify(query);
return fetch('https://sms-verification.parity.io/?' + query, {
method: 'POST', mode: 'cors', cache: 'no-store'
})
.then((res) => {
return res.json().then((data) => {
if (res.ok) {
return data.message;
}
throw new Error(data.message || 'unknown error');
});
});
};

View File

@ -22,8 +22,12 @@ export default class TokenReg {
this.getInstance(); this.getInstance();
} }
getContract () {
return this._registry.getContract('tokenreg');
}
getInstance () { getInstance () {
return this._registry.getContractInstance('tokenreg'); return this.getContract().instance;
} }
tokenCount () { tokenCount () {

View File

@ -21,3 +21,7 @@
.iconMenu option { .iconMenu option {
padding-left: 30px; padding-left: 30px;
} }
.menu {
display: none;
}

View File

@ -46,11 +46,22 @@ a.link, a.link:hover, a.link:visited {
} }
.address { .address {
max-width: 250px;
text-align: left; text-align: left;
div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
} }
.description { .description {
text-align: left; text-align: left;
div {
white-space: nowrap;
}
} }
.center { .center {

View File

@ -58,6 +58,7 @@ export default class Event extends Component {
<td className={ styles.description }> <td className={ styles.description }>
<div>{ isPending ? '' : coin.tla }</div> <div>{ isPending ? '' : coin.tla }</div>
<div>{ isPending ? '' : coin.name }</div> <div>{ isPending ? '' : coin.name }</div>
<div>{ this.renderAddress(event.params.coin) }</div>
</td> </td>
<td className={ styles.address }> <td className={ styles.address }>
{ this.renderAddress(event.params.owner) } { this.renderAddress(event.params.owner) }

View File

@ -46,3 +46,9 @@
.icon { .icon {
margin: 0 0 -4px 1em; margin: 0 0 -4px 1em;
} }
.byline {
opacity: 0.75;
font-size: 0.75em;
padding-top: 0.25em;
}

View File

@ -68,6 +68,9 @@ export default class Owner extends Component {
<Token <Token
address={ token.address } address={ token.address }
tokenreg={ token.tokenreg } /> tokenreg={ token.tokenreg } />
<div className={ styles.byline }>
{ token.address }
</div>
</div> </div>
)); ));
} }

View File

@ -46,8 +46,6 @@ export const loadAccounts = () => (dispatch) => {
address address
})); }));
console.log('accounts', accountsList);
dispatch(setAccounts(accountsList)); dispatch(setAccounts(accountsList));
dispatch(setAccountsInfo(accountsInfo)); dispatch(setAccountsInfo(accountsInfo));
dispatch(setSelectedAccount(accountsList[0].address)); dispatch(setSelectedAccount(accountsList[0].address));

View File

@ -42,12 +42,9 @@ export default class QueryAction extends Component {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
handleQueryToken: PropTypes.func.isRequired, handleQueryToken: PropTypes.func.isRequired,
handleQueryMetaLookup: PropTypes.func.isRequired,
data: PropTypes.object, data: PropTypes.object,
notFound: PropTypes.bool, notFound: PropTypes.bool
metaLoading: PropTypes.bool,
metaData: PropTypes.object
} }
state = initState; state = initState;
@ -131,10 +128,7 @@ export default class QueryAction extends Component {
return ( return (
<Token <Token
fullWidth fullWidth
handleMetaLookup={ this.props.handleQueryMetaLookup } tla={ data.tla }
isMetaLoading={ this.props.metaLoading }
meta={ this.props.metaData }
{ ...data }
/> />
); );
} }

View File

@ -21,7 +21,7 @@ import { Dialog, FlatButton } from 'material-ui';
import AccountSelector from '../../Accounts/AccountSelector'; import AccountSelector from '../../Accounts/AccountSelector';
import InputText from '../../Inputs/Text'; import InputText from '../../Inputs/Text';
import { TOKEN_ADDRESS_TYPE, TLA_TYPE, UINT_TYPE, STRING_TYPE } from '../../Inputs/validation'; import { TOKEN_ADDRESS_TYPE, TLA_TYPE, DECIMAL_TYPE, STRING_TYPE } from '../../Inputs/validation';
import styles from '../actions.css'; import styles from '../actions.css';
@ -41,11 +41,11 @@ const initState = {
floatingLabelText: 'Token TLA', floatingLabelText: 'Token TLA',
hintText: 'The token short name (3 characters)' hintText: 'The token short name (3 characters)'
}, },
base: { decimals: {
...defaultField, ...defaultField,
type: UINT_TYPE, type: DECIMAL_TYPE,
floatingLabelText: 'Token Base', floatingLabelText: 'Token Decimals',
hintText: 'The token precision' hintText: 'The number of decimals (0-18)'
}, },
name: { name: {
...defaultField, ...defaultField,

View File

@ -16,8 +16,6 @@
import { getTokenTotalSupply } from '../utils'; import { getTokenTotalSupply } from '../utils';
const { sha3, bytesToHex } = window.parity.api.util;
export const SET_REGISTER_SENDING = 'SET_REGISTER_SENDING'; export const SET_REGISTER_SENDING = 'SET_REGISTER_SENDING';
export const setRegisterSending = (isSending) => ({ export const setRegisterSending = (isSending) => ({
type: SET_REGISTER_SENDING, type: SET_REGISTER_SENDING,
@ -41,13 +39,12 @@ export const registerCompleted = () => ({
}); });
export const registerToken = (tokenData) => (dispatch, getState) => { export const registerToken = (tokenData) => (dispatch, getState) => {
console.log('registering token', tokenData);
const state = getState(); const state = getState();
const contractInstance = state.status.contract.instance; const contractInstance = state.status.contract.instance;
const fee = state.status.contract.fee; const fee = state.status.contract.fee;
const { address, base, name, tla } = tokenData; const { address, decimals, name, tla } = tokenData;
const base = Math.pow(10, decimals);
dispatch(setRegisterSending(true)); dispatch(setRegisterSending(true));
@ -82,8 +79,6 @@ export const registerToken = (tokenData) => (dispatch, getState) => {
}) })
.then((gasEstimate) => { .then((gasEstimate) => {
options.gas = gasEstimate.mul(1.2).toFixed(0); options.gas = gasEstimate.mul(1.2).toFixed(0);
console.log(`transfer: gas estimated as ${gasEstimate.toFixed(0)} setting to ${options.gas}`);
return contractInstance.register.postTransaction(options, values); return contractInstance.register.postTransaction(options, values);
}) })
.then((result) => { .then((result) => {
@ -182,34 +177,3 @@ export const queryToken = (key, query) => (dispatch, getState) => {
dispatch(setQueryLoading(false)); dispatch(setQueryLoading(false));
}); });
}; };
export const queryTokenMeta = (id, query) => (dispatch, getState) => {
console.log('loading token meta', query);
const state = getState();
const contractInstance = state.status.contract.instance;
const key = sha3(query);
const startDate = Date.now();
dispatch(setQueryMetaLoading(true));
contractInstance
.meta
.call({}, [ id, key ])
.then((value) => {
const meta = {
key, query,
value: value.find(v => v !== 0) ? bytesToHex(value) : null
};
dispatch(setQueryMeta(meta));
setTimeout(() => {
dispatch(setQueryMetaLoading(false));
}, 500 - (Date.now() - startDate));
})
.catch((e) => {
console.error('load meta query error', e);
});
};

View File

@ -37,7 +37,6 @@ export default class Actions extends Component {
handleQueryToken: PropTypes.func.isRequired, handleQueryToken: PropTypes.func.isRequired,
handleQueryClose: PropTypes.func.isRequired, handleQueryClose: PropTypes.func.isRequired,
handleQueryMetaLookup: PropTypes.func.isRequired,
query: PropTypes.object.isRequired query: PropTypes.object.isRequired
}; };
@ -82,7 +81,6 @@ export default class Actions extends Component {
show={ this.state.show[ QUERY_ACTION ] } show={ this.state.show[ QUERY_ACTION ] }
onClose={ this.onQueryClose } onClose={ this.onQueryClose }
handleQueryToken={ this.props.handleQueryToken } handleQueryToken={ this.props.handleQueryToken }
handleQueryMetaLookup={ this.props.handleQueryMetaLookup }
{ ...this.props.query } /> { ...this.props.query } />
</div> </div>
); );

View File

@ -19,7 +19,7 @@ import { connect } from 'react-redux';
import Actions from './component'; import Actions from './component';
import { registerToken, registerReset, queryToken, queryReset, queryTokenMeta } from './actions'; import { registerToken, registerReset, queryToken, queryReset } from './actions';
class TokensContainer extends Component { class TokensContainer extends Component {
@ -49,9 +49,6 @@ const mapDispatchToProps = (dispatch) => {
}, },
handleQueryClose: () => { handleQueryClose: () => {
dispatch(queryReset()); dispatch(queryReset());
},
handleQueryMetaLookup: (id, query) => {
dispatch(queryTokenMeta(id, query));
} }
}; };
}; };

View File

@ -19,6 +19,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding-bottom: 10em;
} }
.warning { .warning {

View File

@ -32,6 +32,7 @@ export const SIMPLE_TOKEN_ADDRESS_TYPE = 'SIMPLE_TOKEN_ADDRESS_TYPE';
export const TLA_TYPE = 'TLA_TYPE'; export const TLA_TYPE = 'TLA_TYPE';
export const SIMPLE_TLA_TYPE = 'SIMPLE_TLA_TYPE'; export const SIMPLE_TLA_TYPE = 'SIMPLE_TLA_TYPE';
export const UINT_TYPE = 'UINT_TYPE'; export const UINT_TYPE = 'UINT_TYPE';
export const DECIMAL_TYPE = 'DECIMAL_TYPE';
export const STRING_TYPE = 'STRING_TYPE'; export const STRING_TYPE = 'STRING_TYPE';
export const HEX_TYPE = 'HEX_TYPE'; export const HEX_TYPE = 'HEX_TYPE';
export const URL_TYPE = 'URL_TYPE'; export const URL_TYPE = 'URL_TYPE';
@ -39,6 +40,7 @@ export const URL_TYPE = 'URL_TYPE';
export const ERRORS = { export const ERRORS = {
invalidTLA: 'The TLA should be 3 characters long', invalidTLA: 'The TLA should be 3 characters long',
invalidUint: 'Please enter a non-negative integer', invalidUint: 'Please enter a non-negative integer',
invalidDecimal: 'Please enter a value between 0 and 18',
invalidString: 'Please enter at least a character', invalidString: 'Please enter at least a character',
invalidAccount: 'Please select an account to transact with', invalidAccount: 'Please select an account to transact with',
invalidRecipient: 'Please select an account to send to', invalidRecipient: 'Please select an account to send to',
@ -152,6 +154,21 @@ const validateUint = (uint) => {
}; };
}; };
const validateDecimal = (decimal) => {
if (!/^\d+$/.test(decimal) || parseInt(decimal) < 0 || parseInt(decimal) > 18) {
return {
error: ERRORS.invalidDecimal,
valid: false
};
}
return {
value: parseInt(decimal),
error: null,
valid: true
};
};
const validateString = (string) => { const validateString = (string) => {
if (string.toString().length === 0) { if (string.toString().length === 0) {
return { return {
@ -204,6 +221,7 @@ export const validate = (value, type, contract) => {
if (type === TLA_TYPE) return validateTLA(value, contract); if (type === TLA_TYPE) return validateTLA(value, contract);
if (type === SIMPLE_TLA_TYPE) return validateTLA(value, contract, true); if (type === SIMPLE_TLA_TYPE) return validateTLA(value, contract, true);
if (type === UINT_TYPE) return validateUint(value); if (type === UINT_TYPE) return validateUint(value);
if (type === DECIMAL_TYPE) return validateDecimal(value);
if (type === STRING_TYPE) return validateString(value); if (type === STRING_TYPE) return validateString(value);
if (type === HEX_TYPE) return validateHex(value); if (type === HEX_TYPE) return validateHex(value);
if (type === URL_TYPE) return validateURL(value); if (type === URL_TYPE) return validateURL(value);

View File

@ -14,11 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { import Contracts from '../../../contracts';
registry as registryAbi,
tokenreg as tokenregAbi,
githubhint as githubhintAbi
} from '../../../contracts/abi';
import { loadToken, setTokenPending, deleteToken, setTokenData } from '../Tokens/actions'; import { loadToken, setTokenPending, deleteToken, setTokenData } from '../Tokens/actions';
@ -34,43 +30,31 @@ export const FIND_CONTRACT = 'FIND_CONTRACT';
export const loadContract = () => (dispatch) => { export const loadContract = () => (dispatch) => {
dispatch(setLoading(true)); dispatch(setLoading(true));
api.parity const { tokenReg, githubHint } = new Contracts(api);
.registryAddress()
.then((registryAddress) => {
console.log(`registry found at ${registryAddress}`);
const registry = api.newContract(registryAbi, registryAddress).instance;
return Promise.all([
registry.getAddress.call({}, [api.util.sha3('tokenreg'), 'A']),
registry.getAddress.call({}, [api.util.sha3('githubhint'), 'A'])
]);
})
.then(([ tokenregAddress, githubhintAddress ]) => {
console.log(`tokenreg was found at ${tokenregAddress}`);
const tokenregContract = api
.newContract(tokenregAbi, tokenregAddress);
const githubhintContract = api
.newContract(githubhintAbi, githubhintAddress);
return Promise
.all([
tokenReg.getContract(),
githubHint.getContract()
])
.then(([ tokenRegContract, githubHintContract ]) => {
dispatch(setContractDetails({ dispatch(setContractDetails({
address: tokenregAddress, address: tokenRegContract.address,
instance: tokenregContract.instance, instance: tokenRegContract.instance,
raw: tokenregContract raw: tokenRegContract
})); }));
dispatch(setGithubhintDetails({ dispatch(setGithubhintDetails({
address: githubhintAddress, address: githubHintContract.address,
instance: githubhintContract.instance, instance: githubHintContract.instance,
raw: githubhintContract raw: githubHintContract
})); }));
dispatch(loadContractDetails()); dispatch(loadContractDetails());
dispatch(subscribeEvents()); dispatch(subscribeEvents());
}) })
.catch((error) => { .catch((error) => {
console.error('loadContract error', error); throw error;
}); });
}; };
@ -78,7 +62,7 @@ export const LOAD_CONTRACT_DETAILS = 'LOAD_CONTRACT_DETAILS';
export const loadContractDetails = () => (dispatch, getState) => { export const loadContractDetails = () => (dispatch, getState) => {
const state = getState(); const state = getState();
const instance = state.status.contract.instance; const { instance } = state.status.contract;
Promise Promise
.all([ .all([
@ -87,8 +71,6 @@ export const loadContractDetails = () => (dispatch, getState) => {
instance.fee.call() instance.fee.call()
]) ])
.then(([accounts, owner, fee]) => { .then(([accounts, owner, fee]) => {
console.log(`owner as ${owner}, fee set at ${fee.toFormat()}`);
const isOwner = accounts.filter(a => a === owner).length > 0; const isOwner = accounts.filter(a => a === owner).length > 0;
dispatch(setContractDetails({ dispatch(setContractDetails({
@ -119,14 +101,14 @@ export const setGithubhintDetails = (details) => ({
export const subscribeEvents = () => (dispatch, getState) => { export const subscribeEvents = () => (dispatch, getState) => {
const state = getState(); const state = getState();
const contract = state.status.contract.raw; const { raw } = state.status.contract;
const previousSubscriptionId = state.status.subscriptionId; const previousSubscriptionId = state.status.subscriptionId;
if (previousSubscriptionId) { if (previousSubscriptionId) {
contract.unsubscribe(previousSubscriptionId); raw.unsubscribe(previousSubscriptionId);
} }
contract raw
.subscribe(null, { .subscribe(null, {
fromBlock: 'latest', fromBlock: 'latest',
toBlock: 'pending', toBlock: 'pending',
@ -187,7 +169,7 @@ export const subscribeEvents = () => (dispatch, getState) => {
)); ));
} }
console.log('new log event', log); console.warn('unknown log event', log);
}); });
}) })
.then((subscriptionId) => { .then((subscriptionId) => {

View File

@ -25,17 +25,15 @@ const initialState = {
isLoading: true, isLoading: true,
subscriptionId: null, subscriptionId: null,
contract: { contract: {
addres: null, address: null,
instance: null, instance: null,
raw: null,
owner: null, owner: null,
isOwner: false, isOwner: false,
fee: null fee: null
}, },
githubhint: { githubhint: {
address: null, address: null,
instance: null, instance: null
raw: null
} }
}; };

View File

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

View File

@ -57,15 +57,28 @@ export default class Token extends Component {
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
isPending: PropTypes.bool, isPending: PropTypes.bool,
isTokenOwner: PropTypes.bool.isRequired, isTokenOwner: PropTypes.bool.isRequired,
isContractOwner: PropTypes.bool.isRequired, isContractOwner: PropTypes.bool,
fullWidth: PropTypes.bool fullWidth: PropTypes.bool
}; };
state = { static defaultProps = {
metaKeyIndex: 0 isContractOwner: false
}; };
state = {
metaKeyIndex: 0,
showMeta: false
};
shouldComponentUpdate (nextProps) {
if (nextProps.isLoading && this.props.isLoading) {
return false;
}
return true;
}
render () { render () {
const { isLoading, fullWidth } = this.props; const { isLoading, fullWidth } = this.props;
@ -152,8 +165,8 @@ export default class Token extends Component {
if (!base || base < 0) return null; if (!base || base < 0) return null;
return ( return (
<Chip <Chip
value={ base.toString() } value={ Math.log10(base).toString() }
label='Base' /> label='Decimals' />
); );
} }
@ -237,7 +250,12 @@ export default class Token extends Component {
} }
renderMeta (meta) { renderMeta (meta) {
const isMetaLoading = this.props.isMetaLoading; const { isMetaLoading } = this.props;
const { showMeta } = this.state;
if (!showMeta) {
return null;
}
if (isMetaLoading) { if (isMetaLoading) {
return (<div> return (<div>
@ -331,6 +349,7 @@ export default class Token extends Component {
const key = metaDataKeys[keyIndex].value; const key = metaDataKeys[keyIndex].value;
const index = this.props.index; const index = this.props.index;
this.setState({ showMeta: true });
this.props.handleMetaLookup(index, key); this.props.handleMetaLookup(index, key);
} }

View File

@ -0,0 +1,73 @@
// Copyright 2015, 2016 Ethcore (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 Token from './token';
import { queryTokenMeta, unregisterToken, addTokenMeta } from '../actions';
class TokenContainer extends Component {
static propTypes = {
handleMetaLookup: PropTypes.func.isRequired,
handleUnregister: PropTypes.func.isRequired,
handleAddMeta: PropTypes.func.isRequired,
tla: PropTypes.string.isRequired
};
render () {
return (
<Token
{ ...this.props }
/>
);
}
}
const mapStateToProps = (_, initProps) => {
const { tla } = initProps;
return (state) => {
const { isOwner } = state.status.contract;
const { tokens } = state.tokens;
const token = tokens.find((t) => t.tla === tla);
return { ...token, isContractOwner: isOwner };
};
};
const mapDispatchToProps = (dispatch) => {
return {
handleMetaLookup: (index, query) => {
dispatch(queryTokenMeta(index, query));
},
handleUnregister: (index) => {
dispatch(unregisterToken(index));
},
handleAddMeta: (index, key, value) => {
dispatch(addTokenMeta(index, key, value));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(TokenContainer);

View File

@ -67,8 +67,6 @@ export const deleteToken = (index) => ({
}); });
export const loadTokens = () => (dispatch, getState) => { export const loadTokens = () => (dispatch, getState) => {
console.log('loading tokens...');
const state = getState(); const state = getState();
const contractInstance = state.status.contract.instance; const contractInstance = state.status.contract.instance;
@ -79,7 +77,6 @@ export const loadTokens = () => (dispatch, getState) => {
.call() .call()
.then((count) => { .then((count) => {
const tokenCount = parseInt(count); const tokenCount = parseInt(count);
console.log(`token count: ${tokenCount}`);
dispatch(setTokenCount(tokenCount)); dispatch(setTokenCount(tokenCount));
for (let i = 0; i < tokenCount; i++) { for (let i = 0; i < tokenCount; i++) {
@ -94,8 +91,6 @@ export const loadTokens = () => (dispatch, getState) => {
}; };
export const loadToken = (index) => (dispatch, getState) => { export const loadToken = (index) => (dispatch, getState) => {
console.log('loading token', index);
const state = getState(); const state = getState();
const contractInstance = state.status.contract.instance; const contractInstance = state.status.contract.instance;
@ -144,7 +139,7 @@ export const loadToken = (index) => (dispatch, getState) => {
} }
data.totalSupply = data.totalSupply.toNumber(); data.totalSupply = data.totalSupply.toNumber();
console.log(`token loaded: #${index}`, data);
dispatch(setTokenData(index, data)); dispatch(setTokenData(index, data));
dispatch(setTokenLoading(index, false)); dispatch(setTokenLoading(index, false));
}) })
@ -159,8 +154,6 @@ export const loadToken = (index) => (dispatch, getState) => {
}; };
export const queryTokenMeta = (index, query) => (dispatch, getState) => { export const queryTokenMeta = (index, query) => (dispatch, getState) => {
console.log('loading token meta', index, query);
const state = getState(); const state = getState();
const contractInstance = state.status.contract.instance; const contractInstance = state.status.contract.instance;
@ -176,7 +169,6 @@ export const queryTokenMeta = (index, query) => (dispatch, getState) => {
value: value.find(v => v !== 0) ? bytesToHex(value) : null value: value.find(v => v !== 0) ? bytesToHex(value) : null
}; };
console.log(`token meta loaded: #${index}`, value);
dispatch(setTokenMeta(index, meta)); dispatch(setTokenMeta(index, meta));
setTimeout(() => { setTimeout(() => {
@ -189,8 +181,6 @@ export const queryTokenMeta = (index, query) => (dispatch, getState) => {
}; };
export const addTokenMeta = (index, key, value) => (dispatch, getState) => { export const addTokenMeta = (index, key, value) => (dispatch, getState) => {
console.log('add token meta', index, key, value);
const state = getState(); const state = getState();
const contractInstance = state.status.contract.instance; const contractInstance = state.status.contract.instance;
const token = state.tokens.tokens.find(t => t.index === index); const token = state.tokens.tokens.find(t => t.index === index);
@ -203,8 +193,6 @@ export const addTokenMeta = (index, key, value) => (dispatch, getState) => {
.estimateGas(options, values) .estimateGas(options, values)
.then((gasEstimate) => { .then((gasEstimate) => {
options.gas = gasEstimate.mul(1.2).toFixed(0); options.gas = gasEstimate.mul(1.2).toFixed(0);
console.log(`addTokenMeta: gas estimated as ${gasEstimate.toFixed(0)} setting to ${options.gas}`);
return contractInstance.setMeta.postTransaction(options, values); return contractInstance.setMeta.postTransaction(options, values);
}) })
.catch((e) => { .catch((e) => {
@ -213,8 +201,6 @@ export const addTokenMeta = (index, key, value) => (dispatch, getState) => {
}; };
export const addGithubhintURL = (from, key, url) => (dispatch, getState) => { export const addGithubhintURL = (from, key, url) => (dispatch, getState) => {
console.log('add githubhint url', key, url);
const state = getState(); const state = getState();
const contractInstance = state.status.githubhint.instance; const contractInstance = state.status.githubhint.instance;
@ -227,8 +213,6 @@ export const addGithubhintURL = (from, key, url) => (dispatch, getState) => {
.estimateGas(options, values) .estimateGas(options, values)
.then((gasEstimate) => { .then((gasEstimate) => {
options.gas = gasEstimate.mul(1.2).toFixed(0); options.gas = gasEstimate.mul(1.2).toFixed(0);
console.log(`transfer: gas estimated as ${gasEstimate.toFixed(0)} setting to ${options.gas}`);
return contractInstance.hintURL.postTransaction(options, values); return contractInstance.hintURL.postTransaction(options, values);
}) })
.catch((e) => { .catch((e) => {
@ -237,24 +221,20 @@ export const addGithubhintURL = (from, key, url) => (dispatch, getState) => {
}; };
export const unregisterToken = (index) => (dispatch, getState) => { export const unregisterToken = (index) => (dispatch, getState) => {
console.log('unregistering token', index); const { contract } = getState().status;
const { instance, owner } = contract;
const state = getState();
const contractInstance = state.status.contract.instance;
const values = [ index ]; const values = [ index ];
const options = { const options = {
from: state.accounts.selected.address from: owner
}; };
contractInstance instance
.unregister .unregister
.estimateGas(options, values) .estimateGas(options, values)
.then((gasEstimate) => { .then((gasEstimate) => {
options.gas = gasEstimate.mul(1.2).toFixed(0); options.gas = gasEstimate.mul(1.2).toFixed(0);
console.log(`transfer: gas estimated as ${gasEstimate.toFixed(0)} setting to ${options.gas}`); return instance.unregister.postTransaction(options, values);
return contractInstance.unregister.postTransaction(options, values);
}) })
.catch((e) => { .catch((e) => {
console.error(`unregisterToken #${index} error`, e); console.error(`unregisterToken #${index} error`, e);

View File

@ -19,16 +19,13 @@ import { connect } from 'react-redux';
import Tokens from './tokens'; import Tokens from './tokens';
import { loadTokens, queryTokenMeta, unregisterToken, addTokenMeta } from './actions'; import { loadTokens } from './actions';
class TokensContainer extends Component { class TokensContainer extends Component {
static propTypes = { static propTypes = {
isOwner: PropTypes.bool,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
tokens: PropTypes.array, tokens: PropTypes.array,
tokenCount: PropTypes.number, onLoadTokens: PropTypes.func
onLoadTokens: PropTypes.func,
accounts: PropTypes.array
}; };
componentDidMount () { componentDidMount () {
@ -36,7 +33,6 @@ class TokensContainer extends Component {
} }
render () { render () {
console.log(this.props);
return ( return (
<Tokens <Tokens
{ ...this.props } { ...this.props }
@ -46,30 +42,19 @@ class TokensContainer extends Component {
} }
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { list } = state.accounts; const { isLoading, tokens } = state.tokens;
const { isLoading, tokens, tokenCount } = state.tokens;
const { isOwner } = state.status.contract; const filteredTokens = tokens
.filter((token) => token && token.tla)
.map((token) => ({ tla: token.tla, owner: token.owner }));
return { isLoading, tokens, tokenCount, isOwner, accounts: list }; return { isLoading, tokens: filteredTokens };
}; };
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {
return { return {
onLoadTokens: () => { onLoadTokens: () => {
dispatch(loadTokens()); dispatch(loadTokens());
},
handleMetaLookup: (index, query) => {
dispatch(queryTokenMeta(index, query));
},
handleUnregister: (index) => {
dispatch(unregisterToken(index));
},
handleAddMeta: (index, key, value) => {
dispatch(addTokenMeta(index, key, value));
} }
}; };
}; };

View File

@ -23,13 +23,8 @@ import styles from './tokens.css';
export default class Tokens extends Component { export default class Tokens extends Component {
static propTypes = { static propTypes = {
handleAddMeta: PropTypes.func.isRequired,
handleUnregister: PropTypes.func.isRequired,
handleMetaLookup: PropTypes.func.isRequired,
isOwner: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired,
tokens: PropTypes.array, tokens: PropTypes.array
accounts: PropTypes.array
}; };
render () { render () {
@ -45,24 +40,12 @@ export default class Tokens extends Component {
} }
renderTokens (tokens) { renderTokens (tokens) {
const { accounts, isOwner } = this.props; return tokens.map((token) => {
return tokens.map((token, index) => {
if (!token || !token.tla) {
return null;
}
const isTokenOwner = !!accounts.find((account) => account.address === token.owner);
return ( return (
<Token <Token
{ ...token } key={ token.tla }
handleUnregister={ this.props.handleUnregister } tla={ token.tla }
handleMetaLookup={ this.props.handleMetaLookup } />
handleAddMeta={ this.props.handleAddMeta }
key={ index }
isTokenOwner={ isTokenOwner }
isContractOwner={ isOwner } />
); );
}); });
} }

View File

@ -25,6 +25,7 @@ import ReactDOM from 'react-dom';
import injectTapEventPlugin from 'react-tap-event-plugin'; import injectTapEventPlugin from 'react-tap-event-plugin';
import { createHashHistory } from 'history'; import { createHashHistory } from 'history';
import { Redirect, Router, Route, useRouterHistory } from 'react-router'; import { Redirect, Router, Route, useRouterHistory } from 'react-router';
import qs from 'querystring';
import SecureApi from './secureApi'; import SecureApi from './secureApi';
import ContractInstances from './contracts'; import ContractInstances from './contracts';
@ -45,6 +46,7 @@ import './index.html';
injectTapEventPlugin(); injectTapEventPlugin();
const AUTH_HASH = '#/auth?';
const parityUrl = process.env.PARITY_URL || const parityUrl = process.env.PARITY_URL ||
( (
process.env.NODE_ENV === 'production' process.env.NODE_ENV === 'production'
@ -52,7 +54,12 @@ const parityUrl = process.env.PARITY_URL ||
: '127.0.0.1:8180' : '127.0.0.1:8180'
); );
const api = new SecureApi(`ws://${parityUrl}`); let token = null;
if (window.location.hash && window.location.hash.indexOf(AUTH_HASH) === 0) {
token = qs.parse(window.location.hash.substr(AUTH_HASH.length)).token;
}
const api = new SecureApi(`ws://${parityUrl}`, token);
ContractInstances.create(api); ContractInstances.create(api);
const store = initStore(api); const store = initStore(api);
@ -67,6 +74,7 @@ ReactDOM.render(
<ContextProvider api={ api } muiTheme={ muiTheme } store={ store }> <ContextProvider api={ api } muiTheme={ muiTheme } store={ store }>
<Router className={ styles.reset } history={ routerHistory }> <Router className={ styles.reset } history={ routerHistory }>
<Redirect from='/' to='/accounts' /> <Redirect from='/' to='/accounts' />
<Redirect from='/auth' to='/accounts' query={ {} } />
<Redirect from='/settings' to='/settings/views' /> <Redirect from='/settings' to='/settings/views' />
<Route path='/' component={ Application }> <Route path='/' component={ Application }>
<Route path='accounts' component={ Accounts } /> <Route path='accounts' component={ Accounts } />

View File

@ -14,9 +14,45 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { BlockNumber, Hash, Integer } from '../types'; import { BlockNumber, Data, Hash, Integer } from '../types';
export default { export default {
block: {
desc: 'Returns traces created at given block',
params: [
{
type: BlockNumber,
desc: 'Integer block number, or \'latest\' for the last mined block or \'pending\', \'earliest\' for not yet mined transactions'
}
],
returns: {
type: Array,
desc: 'Block traces'
}
},
call: {
desc: 'Returns traces for a specific call',
params: [
{
type: Object,
desc: 'Call options'
},
{
type: BlockNumber,
desc: 'The blockNumber'
},
{
type: Array,
desc: 'Type of trace, one or more of \'vmTrace\', \'trace\' and/or \'stateDiff\''
}
],
returns: {
type: Array,
desc: 'Block traces'
}
},
filter: { filter: {
desc: 'Returns traces matching given filter', desc: 'Returns traces matching given filter',
params: [ params: [
@ -49,6 +85,42 @@ export default {
} }
}, },
rawTransaction: {
desc: 'Traces a call to eth_sendRawTransaction without making the call, returning the traces',
params: [
{
type: Data,
desc: 'Transaction data'
},
{
type: Array,
desc: 'Type of trace, one or more of \'vmTrace\', \'trace\' and/or \'stateDiff\''
}
],
returns: {
type: Array,
desc: 'Block traces'
}
},
replayTransaction: {
desc: 'Replays a transaction, returning the traces',
params: [
{
type: Hash,
desc: 'Transaction hash'
},
{
type: Array,
desc: 'Type of trace, one or more of \'vmTrace\', \'trace\' and/or \'stateDiff\''
}
],
returns: {
type: Array,
desc: 'Block traces'
}
},
transaction: { transaction: {
desc: 'Returns all traces of given transaction', desc: 'Returns all traces of given transaction',
params: [ params: [
@ -61,19 +133,5 @@ export default {
type: Array, type: Array,
desc: 'Traces of given transaction' desc: 'Traces of given transaction'
} }
},
block: {
desc: 'Returns traces created at given block',
params: [
{
type: BlockNumber,
desc: 'Integer block number, or \'latest\' for the last mined block or \'pending\', \'earliest\' for not yet mined transactions'
}
],
returns: {
type: Array,
desc: 'Block traces'
}
} }
}; };

View File

@ -227,6 +227,7 @@ export default class AddContract extends Component {
onEditAbi = (abiIn) => { onEditAbi = (abiIn) => {
const { api } = this.context; const { api } = this.context;
const { abi, abiError, abiParsed } = validateAbi(abiIn, api); const { abi, abiError, abiParsed } = validateAbi(abiIn, api);
this.setState({ abi, abiError, abiParsed }); this.setState({ abi, abiError, abiParsed });
} }

View File

@ -15,10 +15,10 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { MenuItem } from 'material-ui';
import { AddressSelect, Form, Input, InputAddressSelect, Select } from '../../../ui'; import { AddressSelect, Form, Input, TypedInput } from '../../../ui';
import { validateAbi } from '../../../util/validation'; import { validateAbi } from '../../../util/validation';
import { parseAbiType } from '../../../util/abi';
import styles from '../deployContract.css'; import styles from '../deployContract.css';
@ -103,6 +103,7 @@ export default class DetailsStep extends Component {
value={ code } value={ code }
onSubmit={ this.onCodeChange } onSubmit={ this.onCodeChange }
readOnly={ readOnly } /> readOnly={ readOnly } />
{ this.renderConstructorInputs() } { this.renderConstructorInputs() }
</Form> </Form>
); );
@ -117,59 +118,23 @@ export default class DetailsStep extends Component {
} }
return inputs.map((input, index) => { return inputs.map((input, index) => {
const onChange = (event, value) => this.onParamChange(index, value); const onChange = (value) => this.onParamChange(index, value);
const onChangeBool = (event, _index, value) => this.onParamChange(index, value === 'true');
const onSubmit = (value) => this.onParamChange(index, value);
const label = `${input.name}: ${input.type}`;
let inputBox = null;
switch (input.type) { const label = `${input.name ? `${input.name}: ` : ''}${input.type}`;
case 'address': const value = params[index];
inputBox = ( const error = paramsError[index];
<InputAddressSelect const param = parseAbiType(input.type);
accounts={ accounts }
editing
label={ label }
value={ params[index] }
error={ paramsError[index] }
onChange={ onChange } />
);
break;
case 'bool':
const boolitems = ['false', 'true'].map((bool) => {
return (
<MenuItem
key={ bool }
value={ bool }
label={ bool }>{ bool }</MenuItem>
);
});
inputBox = (
<Select
label={ label }
value={ params[index] ? 'true' : 'false' }
error={ paramsError[index] }
onChange={ onChangeBool }>
{ boolitems }
</Select>
);
break;
default:
inputBox = (
<Input
label={ label }
value={ params[index] }
error={ paramsError[index] }
onSubmit={ onSubmit } />
);
break;
}
return ( return (
<div key={ index } className={ styles.funcparams }> <div key={ index } className={ styles.funcparams }>
{ inputBox } <TypedInput
label={ label }
value={ value }
error={ error }
accounts={ accounts }
onChange={ onChange }
param={ param }
/>
</div> </div>
); );
}); });
@ -200,35 +165,14 @@ export default class DetailsStep extends Component {
const { abiError, abiParsed } = validateAbi(abi, api); const { abiError, abiParsed } = validateAbi(abi, api);
if (!abiError) { if (!abiError) {
const { inputs } = abiParsed.find((method) => method.type === 'constructor') || { inputs: [] }; const { inputs } = abiParsed
.find((method) => method.type === 'constructor') || { inputs: [] };
const params = []; const params = [];
inputs.forEach((input) => { inputs.forEach((input) => {
switch (input.type) { const param = parseAbiType(input.type);
case 'address': params.push(param.default);
params.push('0x');
break;
case 'bool':
params.push(false);
break;
case 'bytes':
params.push('0x');
break;
case 'uint':
params.push('0');
break;
case 'string':
params.push('');
break;
default:
params.push('0');
break;
}
}); });
onParamsChange(params); onParamsChange(params);

View File

@ -26,6 +26,8 @@ import ErrorStep from './ErrorStep';
import styles from './deployContract.css'; import styles from './deployContract.css';
import { ERROR_CODES } from '../../api/transport/error';
const steps = ['contract details', 'deployment', 'completed']; const steps = ['contract details', 'deployment', 'completed'];
export default class DeployContract extends Component { export default class DeployContract extends Component {
@ -63,7 +65,8 @@ export default class DeployContract extends Component {
params: [], params: [],
paramsError: [], paramsError: [],
step: 0, step: 0,
deployError: null deployError: null,
rejected: false
} }
componentWillMount () { componentWillMount () {
@ -92,16 +95,22 @@ export default class DeployContract extends Component {
} }
render () { render () {
const { step, deployError } = this.state; const { step, deployError, rejected } = this.state;
const realSteps = deployError || rejected ? null : steps;
const title = realSteps
? null
: (deployError ? 'deployment failed' : 'rejected');
return ( return (
<Modal <Modal
actions={ this.renderDialogActions() } actions={ this.renderDialogActions() }
current={ step } current={ step }
steps={ deployError ? null : steps } steps={ realSteps }
title={ deployError ? 'deployment failed' : null } title={ title }
waiting={ [1] } waiting={ realSteps ? [1] : null }
visible> visible
scroll>
{ this.renderStep() } { this.renderStep() }
</Modal> </Modal>
); );
@ -118,8 +127,22 @@ export default class DeployContract extends Component {
onClick={ this.onClose } /> onClick={ this.onClose } />
); );
const closeBtn = (
<Button
icon={ <ContentClear /> }
label='Close'
onClick={ this.onClose } />
);
const closeBtnOk = (
<Button
icon={ <ActionDoneAll /> }
label='Close'
onClick={ this.onClose } />
);
if (deployError) { if (deployError) {
return cancelBtn; return closeBtn;
} }
switch (step) { switch (step) {
@ -134,23 +157,16 @@ export default class DeployContract extends Component {
]; ];
case 1: case 1:
return [ return [ closeBtn ];
cancelBtn
];
case 2: case 2:
return [ return [ closeBtnOk ];
<Button
icon={ <ActionDoneAll /> }
label='Close'
onClick={ this.onClose } />
];
} }
} }
renderStep () { renderStep () {
const { accounts, readOnly } = this.props; const { accounts, readOnly } = this.props;
const { address, deployError, step, deployState, txhash } = this.state; const { address, deployError, step, deployState, txhash, rejected } = this.state;
if (deployError) { if (deployError) {
return ( return (
@ -158,6 +174,15 @@ export default class DeployContract extends Component {
); );
} }
if (rejected) {
return (
<BusyStep
title='The deployment has been rejected'
state='You can safely close this window, the contract deployment will not occur.'
/>
);
}
switch (step) { switch (step) {
case 0: case 0:
return ( return (
@ -265,6 +290,11 @@ export default class DeployContract extends Component {
}); });
}) })
.catch((error) => { .catch((error) => {
if (error.code === ERROR_CODES.REQUEST_REJECTED) {
this.setState({ rejected: true });
return false;
}
console.error('error deploying contract', error); console.error('error deploying contract', error);
this.setState({ deployError: error }); this.setState({ deployError: error });
store.dispatch({ type: 'newError', error }); store.dispatch({ type: 'newError', error });
@ -277,8 +307,6 @@ export default class DeployContract extends Component {
return; return;
} }
console.log('onDeploymentState', data);
switch (data.state) { switch (data.state) {
case 'estimateGas': case 'estimateGas':
case 'postTransaction': case 'postTransaction':

View File

@ -23,6 +23,8 @@ import { validateAddress, validateUint } from '../../util/validation';
import DetailsStep from './DetailsStep'; import DetailsStep from './DetailsStep';
import { ERROR_CODES } from '../../api/transport/error';
export default class ExecuteContract extends Component { export default class ExecuteContract extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired, api: PropTypes.object.isRequired,
@ -49,7 +51,8 @@ export default class ExecuteContract extends Component {
step: 0, step: 0,
sending: false, sending: false,
busyState: null, busyState: null,
txhash: null txhash: null,
rejected: false
} }
componentDidMount () { componentDidMount () {
@ -80,6 +83,7 @@ export default class ExecuteContract extends Component {
const { onClose, fromAddress } = this.props; const { onClose, fromAddress } = this.props;
const { sending, step, fromAddressError, valuesError } = this.state; const { sending, step, fromAddressError, valuesError } = this.state;
const hasError = fromAddressError || valuesError.find((error) => error); const hasError = fromAddressError || valuesError.find((error) => error);
const cancelBtn = ( const cancelBtn = (
<Button <Button
key='cancel' key='cancel'
@ -115,7 +119,16 @@ export default class ExecuteContract extends Component {
renderStep () { renderStep () {
const { onFromAddressChange } = this.props; const { onFromAddressChange } = this.props;
const { step, busyState, txhash } = this.state; const { step, busyState, txhash, rejected } = this.state;
if (rejected) {
return (
<BusyStep
title='The execution has been rejected'
state='You can safely close this window, the function execution will not occur.'
/>
);
}
if (step === 0) { if (step === 0) {
return ( return (
@ -221,7 +234,17 @@ export default class ExecuteContract extends Component {
}) })
.then((requestId) => { .then((requestId) => {
this.setState({ busyState: 'Waiting for authorization in the Parity Signer' }); this.setState({ busyState: 'Waiting for authorization in the Parity Signer' });
return api.pollMethod('parity_checkRequest', requestId);
return api
.pollMethod('parity_checkRequest', requestId)
.catch((e) => {
if (e.code === ERROR_CODES.REQUEST_REJECTED) {
this.setState({ rejected: true });
return false;
}
throw e;
});
}) })
.then((txhash) => { .then((txhash) => {
this.setState({ sending: false, step: 2, txhash, busyState: 'Your transaction has been posted to the network' }); this.setState({ sending: false, step: 2, txhash, busyState: 'Your transaction has been posted to the network' });

View File

@ -0,0 +1,31 @@
/* Copyright 2015, 2016 Ethcore (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/>.
*/
.spacing {
margin-top: 1.5em;
}
.container {
margin-top: .5em;
display: flex;
align-items: center;
}
.message {
margin-top: 0;
margin-bottom: 0;
margin-left: .5em;
}

View File

@ -0,0 +1,31 @@
// Copyright 2015, 2016 Ethcore (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 } from 'react';
import SuccessIcon from 'material-ui/svg-icons/navigation/check';
import styles from './done.css';
export default class Done extends Component {
render () {
return (
<div className={ styles.container }>
<SuccessIcon />
<p className={ styles.message }>Congratulations, your account is verified!</p>
</div>
);
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (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 './done';

View File

@ -0,0 +1,49 @@
/* Copyright 2015, 2016 Ethcore (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/>.
*/
.list li {
padding: .1em 0;
}
.spacing {
margin-top: 1.5em;
}
.container {
margin-top: .5em;
display: flex;
align-items: center;
}
.message {
margin-top: 0;
margin-bottom: 0;
margin-left: .5em;
}
.terms {
line-height: 1.3;
opacity: .7;
ul {
padding-left: 1.5em;
}
li {
margin-top: .2em;
margin-bottom: .2em;
}
}

View File

@ -0,0 +1,151 @@
// Copyright 2015, 2016 Ethcore (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 nullable from '../../../util/nullable-proptype';
import BigNumber from 'bignumber.js';
import { Checkbox } from 'material-ui';
import InfoIcon from 'material-ui/svg-icons/action/info-outline';
import SuccessIcon from 'material-ui/svg-icons/navigation/check';
import ErrorIcon from 'material-ui/svg-icons/navigation/close';
import { fromWei } from '../../../api/util/wei';
import { Form, Input } from '../../../ui';
import terms from '../terms-of-service';
import styles from './gatherData.css';
export default class GatherData extends Component {
static propTypes = {
fee: React.PropTypes.instanceOf(BigNumber),
isNumberValid: PropTypes.bool.isRequired,
isVerified: nullable(PropTypes.bool.isRequired),
hasRequested: nullable(PropTypes.bool.isRequired),
setNumber: PropTypes.func.isRequired,
setConsentGiven: PropTypes.func.isRequired
}
render () {
const { isNumberValid, isVerified } = this.props;
return (
<Form>
<p>The following steps will let you prove that you control both an account and a phone number.</p>
<ol className={ styles.list }>
<li>You send a verification request to a specific contract.</li>
<li>Our server puts a puzzle into this contract.</li>
<li>The code you receive via SMS is the solution to this puzzle.</li>
</ol>
{ this.renderFee() }
{ this.renderCertified() }
{ this.renderRequested() }
<Input
label={ 'phone number' }
hint={ 'the SMS will be sent to this number' }
error={ isNumberValid ? null : 'invalid number' }
disabled={ isVerified }
onChange={ this.numberOnChange }
onSubmit={ this.numberOnSubmit }
/>
<Checkbox
className={ styles.spacing }
label={ 'I agree to the terms and conditions below.' }
disabled={ isVerified }
onCheck={ this.consentOnChange }
/>
<div className={ styles.terms }>{ terms }</div>
</Form>
);
}
renderFee () {
const { fee } = this.props;
if (!fee) {
return (<p>Fetching the fee</p>);
}
return (
<div className={ styles.container }>
<InfoIcon />
<p className={ styles.message }>The fee is { fromWei(fee).toFixed(3) } ETH.</p>
</div>
);
}
renderCertified () {
const { isVerified } = this.props;
if (isVerified) {
return (
<div className={ styles.container }>
<ErrorIcon />
<p className={ styles.message }>Your account is already verified.</p>
</div>
);
} else if (isVerified === false) {
return (
<div className={ styles.container }>
<SuccessIcon />
<p className={ styles.message }>Your account is not verified yet.</p>
</div>
);
}
return (
<p className={ styles.message }>Checking if your account is verified</p>
);
}
renderRequested () {
const { isVerified, hasRequested } = this.props;
// If the account is verified, don't show that it has requested verification.
if (isVerified) {
return null;
}
if (hasRequested) {
return (
<div className={ styles.container }>
<InfoIcon />
<p className={ styles.message }>You already requested verification.</p>
</div>
);
}
if (hasRequested === false) {
return (
<div className={ styles.container }>
<SuccessIcon />
<p className={ styles.message }>You did not request verification yet.</p>
</div>
);
}
return (
<p className={ styles.message }>Checking if you requested verification</p>
);
}
numberOnSubmit = (value) => {
this.props.setNumber(value);
}
numberOnChange = (_, value) => {
this.props.setNumber(value);
}
consentOnChange = (_, consentGiven) => {
this.props.setConsentGiven(consentGiven);
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (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 './gatherData';

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (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 './queryCode';

View File

@ -0,0 +1,52 @@
// Copyright 2015, 2016 Ethcore (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 { Form, Input } from '../../../ui';
export default class QueryCode extends Component {
static propTypes = {
number: PropTypes.string.isRequired,
isCodeValid: PropTypes.bool.isRequired,
setCode: PropTypes.func.isRequired
}
render () {
const { number, isCodeValid } = this.props;
return (
<Form>
<p>The verification code has been sent to { number }.</p>
<Input
label={ 'verification code' }
hint={ 'Enter the code you received via SMS.' }
error={ isCodeValid ? null : 'invalid code' }
onChange={ this.onChange }
onSubmit={ this.onSubmit }
/>
</Form>
);
}
onChange = (_, code) => {
this.props.setCode(code.trim());
}
onSubmit = (code) => {
this.props.setCode(code.trim());
}
}

View File

@ -0,0 +1,176 @@
// Copyright 2015, 2016 Ethcore (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 { observer } from 'mobx-react';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear';
import { Button, IdentityIcon, Modal } from '../../ui';
import {
LOADING,
QUERY_DATA,
POSTING_REQUEST, POSTED_REQUEST,
REQUESTING_SMS, REQUESTED_SMS,
POSTING_CONFIRMATION, POSTED_CONFIRMATION,
DONE
} from './store';
import GatherData from './GatherData';
import SendRequest from './SendRequest';
import QueryCode from './QueryCode';
import SendConfirmation from './SendConfirmation';
import Done from './Done';
@observer
export default class SMSVerification extends Component {
static propTypes = {
store: PropTypes.any.isRequired,
account: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired
}
static phases = { // mapping (store steps -> steps)
[LOADING]: 0,
[QUERY_DATA]: 1,
[POSTING_REQUEST]: 2, [POSTED_REQUEST]: 2, [REQUESTING_SMS]: 2,
[REQUESTED_SMS]: 3,
[POSTING_CONFIRMATION]: 4, [POSTED_CONFIRMATION]: 4,
[DONE]: 5
}
render () {
const phase = SMSVerification.phases[this.props.store.step];
const { error, isStepValid } = this.props.store;
return (
<Modal
actions={ this.renderDialogActions(phase, error, isStepValid) }
title='verify your account via SMS'
visible scroll
current={ phase }
steps={ ['Prepare', 'Enter Data', 'Request', 'Enter Code', 'Confirm', 'Done!'] }
waiting={ error ? [] : [ 0, 2, 4 ] }
>
{ this.renderStep(phase, error) }
</Modal>
);
}
renderDialogActions (phase, error, isStepValid) {
const { store, account, onClose } = this.props;
const cancel = (
<Button
key='cancel' label='Cancel'
icon={ <ContentClear /> }
onClick={ onClose }
/>
);
if (error) {
return (<div>{ cancel }</div>);
}
if (phase === 5) {
return (
<div>
{ cancel }
<Button
key='done' label='Done'
disabled={ !isStepValid }
icon={ <ActionDoneAll /> }
onClick={ onClose }
/>
</div>
);
}
let action = () => {};
switch (phase) {
case 1:
action = store.sendRequest;
break;
case 2:
action = store.queryCode;
break;
case 3:
action = store.sendConfirmation;
break;
case 4:
action = store.done;
break;
}
return (
<div>
{ cancel }
<Button
key='next' label='Next'
disabled={ !isStepValid }
icon={ <IdentityIcon address={ account } button /> }
onClick={ action }
/>
</div>
);
}
renderStep (phase, error) {
if (error) {
return (<p>{ error }</p>);
}
const {
step,
fee, number, isNumberValid, isVerified, hasRequested,
requestTx, isCodeValid, confirmationTx,
setNumber, setConsentGiven, setCode
} = this.props.store;
if (phase === 5) {
return (<Done />);
}
if (phase === 4) {
return (<SendConfirmation step={ step } tx={ confirmationTx } />);
}
if (phase === 3) {
return (
<QueryCode
number={ number } fee={ fee } isCodeValid={ isCodeValid }
setCode={ setCode }
/>
);
}
if (phase === 2) {
return (<SendRequest step={ step } tx={ requestTx } />);
}
if (phase === 1) {
const { setNumber, setConsentGiven } = this.props.store;
return (
<GatherData
fee={ fee } isNumberValid={ isNumberValid }
isVerified={ isVerified } hasRequested={ hasRequested }
setNumber={ setNumber } setConsentGiven={ setConsentGiven }
/>
);
}
if (phase === 0) {
return (<p>Preparing awesomeness!</p>);
}
return null;
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (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 './sendConfirmation';

View File

@ -0,0 +1,20 @@
/* Copyright 2015, 2016 Ethcore (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/>.
*/
.centered {
text-align: center;
}

View File

@ -0,0 +1,51 @@
// Copyright 2015, 2016 Ethcore (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 nullable from '../../../util/nullable-proptype';
import TxHash from '../../../ui/TxHash';
import {
POSTING_CONFIRMATION, POSTED_CONFIRMATION
} from '../store';
import styles from './sendConfirmation.css';
export default class SendConfirmation extends Component {
static propTypes = {
step: PropTypes.any.isRequired,
tx: nullable(PropTypes.any.isRequired)
}
render () {
const { step, tx } = this.props;
if (step === POSTING_CONFIRMATION) {
return (<p>The verification code will be sent to the contract. Please authorize this using the Parity Signer.</p>);
}
if (step === POSTED_CONFIRMATION) {
return (
<div className={ styles.centered }>
<TxHash hash={ tx } maxConfirmations={ 2 } />
<p>Please keep this window open.</p>
</div>
);
}
return null;
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (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 './sendRequest';

View File

@ -0,0 +1,20 @@
/* Copyright 2015, 2016 Ethcore (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/>.
*/
.centered {
text-align: center;
}

View File

@ -0,0 +1,57 @@
// Copyright 2015, 2016 Ethcore (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 nullable from '../../../util/nullable-proptype';
import TxHash from '../../../ui/TxHash';
import {
POSTING_REQUEST, POSTED_REQUEST, REQUESTING_SMS
} from '../store';
import styles from './sendRequest.css';
export default class SendRequest extends Component {
static propTypes = {
step: PropTypes.any.isRequired,
tx: nullable(PropTypes.any.isRequired)
}
render () {
const { step, tx } = this.props;
switch (step) {
case POSTING_REQUEST:
return (<p>A verification request will be sent to the contract. Please authorize this using the Parity Signer.</p>);
case POSTED_REQUEST:
return (
<div className={ styles.centered }>
<TxHash hash={ tx } maxConfirmations={ 1 } />
<p>Please keep this window open.</p>
</div>
);
case REQUESTING_SMS:
return (
<p>Requesting an SMS from the Parity server.</p>
);
default:
return null;
}
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (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 './SMSVerification';

View File

@ -0,0 +1,246 @@
// Copyright 2015, 2016 Ethcore (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 { observable, computed, autorun, action } from 'mobx';
import phone from 'phoneformat.js';
import { sha3 } from '../../api/util/sha3';
import Contracts from '../../contracts';
import { checkIfVerified, checkIfRequested, postToServer } from '../../contracts/sms-verification';
import checkIfTxFailed from '../../util/check-if-tx-failed';
import waitForConfirmations from '../../util/wait-for-block-confirmations';
const validCode = /^[A-Z\s]+$/i;
export const LOADING = 'fetching-contract';
export const QUERY_DATA = 'query-data';
export const POSTING_REQUEST = 'posting-request';
export const POSTED_REQUEST = 'posted-request';
export const REQUESTING_SMS = 'requesting-sms';
export const REQUESTED_SMS = 'requested-sms';
export const QUERY_CODE = 'query-code';
export const POSTING_CONFIRMATION = 'posting-confirmation';
export const POSTED_CONFIRMATION = 'posted-confirmation';
export const DONE = 'done';
export default class VerificationStore {
@observable step = null;
@observable error = null;
@observable contract = null;
@observable fee = null;
@observable isVerified = null;
@observable hasRequested = null;
@observable consentGiven = false;
@observable number = '';
@observable requestTx = null;
@observable code = '';
@observable confirmationTx = null;
@computed get isCodeValid () {
return validCode.test(this.code);
}
@computed get isNumberValid () {
return phone.isValidNumber(this.number);
}
@computed get isStepValid () {
if (this.step === DONE) {
return true;
}
if (this.error) {
return false;
}
switch (this.step) {
case LOADING:
return this.contract && this.fee && this.isVerified !== null && this.hasRequested !== null;
case QUERY_DATA:
return this.isNumberValid && this.consentGiven;
case REQUESTED_SMS:
return this.requestTx;
case QUERY_CODE:
return this.isCodeValid;
case POSTED_CONFIRMATION:
return this.confirmationTx;
default:
return false;
}
}
constructor (api, account) {
this.api = api;
this.account = account;
this.step = LOADING;
Contracts.create(api).registry.getContract('smsVerification')
.then((contract) => {
this.contract = contract;
this.load();
})
.catch((err) => {
this.error = 'Failed to fetch the contract: ' + err.message;
});
autorun(() => {
if (this.error) {
console.error('sms verification: ' + this.error);
}
});
}
@action load = () => {
const { contract, account } = this;
this.step = LOADING;
const fee = contract.instance.fee.call()
.then((fee) => {
this.fee = fee;
})
.catch((err) => {
this.error = 'Failed to fetch the fee: ' + err.message;
});
const isVerified = checkIfVerified(contract, account)
.then((isVerified) => {
this.isVerified = isVerified;
})
.catch((err) => {
this.error = 'Failed to check if verified: ' + err.message;
});
const hasRequested = checkIfRequested(contract, account)
.then((txHash) => {
this.hasRequested = !!txHash;
if (txHash) {
this.requestTx = txHash;
}
})
.catch((err) => {
this.error = 'Failed to check if requested: ' + err.message;
});
Promise
.all([ fee, isVerified, hasRequested ])
.then(() => {
this.step = QUERY_DATA;
});
}
@action setNumber = (number) => {
this.number = number;
}
@action setConsentGiven = (consentGiven) => {
this.consentGiven = consentGiven;
}
@action setCode = (code) => {
this.code = code;
}
@action sendRequest = () => {
const { api, account, contract, fee, number, hasRequested } = this;
const request = contract.functions.find((fn) => fn.name === 'request');
const options = { from: account, value: fee.toString() };
let chain = Promise.resolve();
if (!hasRequested) {
this.step = POSTING_REQUEST;
chain = request.estimateGas(options, [])
.then((gas) => {
options.gas = gas.mul(1.2).toFixed(0);
return request.postTransaction(options, []);
})
.then((handle) => {
// TODO: The "request rejected" error doesn't have any property to
// distinguish it from other errors, so we can't give a meaningful error here.
return api.pollMethod('parity_checkRequest', handle);
})
.then((txHash) => {
this.requestTx = txHash;
return checkIfTxFailed(api, txHash, options.gas)
.then((hasFailed) => {
if (hasFailed) {
throw new Error('Transaction failed, all gas used up.');
}
this.step = POSTED_REQUEST;
return waitForConfirmations(api, txHash, 1);
});
});
}
chain
.then(() => {
this.step = REQUESTING_SMS;
return postToServer({ number, address: account });
})
.then(() => {
this.step = REQUESTED_SMS;
})
.catch((err) => {
this.error = 'Failed to request a confirmation SMS: ' + err.message;
});
}
@action queryCode = () => {
this.step = QUERY_CODE;
}
@action sendConfirmation = () => {
const { api, account, contract, code } = this;
const token = sha3(code);
const confirm = contract.functions.find((fn) => fn.name === 'confirm');
const options = { from: account };
const values = [ token ];
this.step = POSTING_CONFIRMATION;
confirm.estimateGas(options, values)
.then((gas) => {
options.gas = gas.mul(1.2).toFixed(0);
return confirm.postTransaction(options, values);
})
.then((handle) => {
// TODO: The "request rejected" error doesn't have any property to
// distinguish it from other errors, so we can't give a meaningful error here.
return api.pollMethod('parity_checkRequest', handle);
})
.then((txHash) => {
this.confirmationTx = txHash;
return checkIfTxFailed(api, txHash, options.gas)
.then((hasFailed) => {
if (hasFailed) {
throw new Error('Transaction failed, all gas used up.');
}
this.step = POSTED_CONFIRMATION;
return waitForConfirmations(api, txHash, 1);
});
})
.then(() => {
this.step = DONE;
})
.catch((err) => {
this.error = 'Failed to send the verification code: ' + err.message;
});
}
@action done = () => {
this.step = DONE;
}
}

View File

@ -0,0 +1,27 @@
// Copyright 2015, 2016 Ethcore (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 from 'react';
export default (
<ul>
<li>This privacy notice relates to your use of the Parity SMS verification service. We take your privacy seriously and deal in an honest, direct and transparent way when it comes to your data.</li>
<li>We collect your phone number when you use this service. This is temporarily kept in memory, and then encrypted and stored in our EU servers. We only retain the cryptographic hash of the number to prevent duplicated accounts. You consent to this use.</li>
<li>You pay a fee for the cost of this service using the account you want to verify.</li>
<li>Your phone number is transmitted to a third party US SMS verification service Twilio for the sole purpose of the SMS verification. You consent to this use. Twilios privacy policy is here: <a href={ 'https://www.twilio.com/legal/privacy/developer' }>https://www.twilio.com/legal/privacy/developer</a>.</li>
<li><i>Parity Technology Limited</i> is registered in England and Wales under company number <code>09760015</code> and complies with the Data Protection Act 1998 (UK). You may contact us via email at <a href={ 'mailto:admin@parity.io' }>admin@parity.io</a>. Our general privacy policy can be found here: <a href={ 'https://ethcore.io/legal.html' }>https://ethcore.io/legal.html</a>.</li>
</ul>
);

View File

@ -28,13 +28,16 @@ import Extras from './Extras';
import ERRORS from './errors'; import ERRORS from './errors';
import styles from './transfer.css'; import styles from './transfer.css';
import { ERROR_CODES } from '../../api/transport/error';
const DEFAULT_GAS = '21000'; const DEFAULT_GAS = '21000';
const DEFAULT_GASPRICE = '20000000000'; const DEFAULT_GASPRICE = '20000000000';
const TITLES = { const TITLES = {
transfer: 'transfer details', transfer: 'transfer details',
sending: 'sending', sending: 'sending',
complete: 'complete', complete: 'complete',
extras: 'extra information' extras: 'extra information',
rejected: 'rejected'
}; };
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete]; const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
const STAGES_EXTRA = [TITLES.transfer, TITLES.extras, TITLES.sending, TITLES.complete]; const STAGES_EXTRA = [TITLES.transfer, TITLES.extras, TITLES.sending, TITLES.complete];
@ -74,7 +77,8 @@ export default class Transfer extends Component {
valueAll: false, valueAll: false,
valueError: null, valueError: null,
isEth: true, isEth: true,
busyState: null busyState: null,
rejected: false
} }
componentDidMount () { componentDidMount () {
@ -82,13 +86,19 @@ export default class Transfer extends Component {
} }
render () { render () {
const { stage, extras } = this.state; const { stage, extras, rejected } = this.state;
const steps = [].concat(extras ? STAGES_EXTRA : STAGES_BASIC);
if (rejected) {
steps[steps.length - 1] = TITLES.rejected;
}
return ( return (
<Modal <Modal
actions={ this.renderDialogActions() } actions={ this.renderDialogActions() }
current={ stage } current={ stage }
steps={ extras ? STAGES_EXTRA : STAGES_BASIC } steps={ steps }
waiting={ extras ? [2] : [1] } waiting={ extras ? [2] : [1] }
visible visible
scroll scroll
@ -133,7 +143,16 @@ export default class Transfer extends Component {
} }
renderCompletePage () { renderCompletePage () {
const { sending, txhash, busyState } = this.state; const { sending, txhash, busyState, rejected } = this.state;
if (rejected) {
return (
<BusyStep
title='The transaction has been rejected'
state='You can safely close this window, the transfer will not occur.'
/>
);
}
if (sending) { if (sending) {
return ( return (
@ -314,7 +333,7 @@ export default class Transfer extends Component {
} }
const token = balance.tokens.find((balance) => balance.token.tag === tag).token; const token = balance.tokens.find((balance) => balance.token.tag === tag).token;
const s = new BigNumber(num).mul(token.format || 1).toString(); const s = new BigNumber(num).mul(token.format || 1).toFixed();
if (s.indexOf('.') !== -1) { if (s.indexOf('.') !== -1) {
return ERRORS.invalidDecimals; return ERRORS.invalidDecimals;
@ -455,7 +474,17 @@ export default class Transfer extends Component {
: this._sendToken() : this._sendToken()
).then((requestId) => { ).then((requestId) => {
this.setState({ busyState: 'Waiting for authorization in the Parity Signer' }); this.setState({ busyState: 'Waiting for authorization in the Parity Signer' });
return api.pollMethod('parity_checkRequest', requestId);
return api
.pollMethod('parity_checkRequest', requestId)
.catch((e) => {
if (e.code === ERROR_CODES.REQUEST_REJECTED) {
this.setState({ rejected: true });
return false;
}
throw e;
});
}) })
.then((txhash) => { .then((txhash) => {
this.onNext(); this.onNext();
@ -516,6 +545,13 @@ export default class Transfer extends Component {
} }
recalculateGas = () => { recalculateGas = () => {
if (!this.isValid()) {
this.setState({
gas: '0'
}, this.recalculate);
return;
}
(this.state.isEth (this.state.isEth
? this._estimateGasEth() ? this._estimateGasEth()
: this._estimateGasToken() : this._estimateGasToken()

View File

@ -22,6 +22,7 @@ import EditMeta from './EditMeta';
import ExecuteContract from './ExecuteContract'; import ExecuteContract from './ExecuteContract';
import FirstRun from './FirstRun'; import FirstRun from './FirstRun';
import Shapeshift from './Shapeshift'; import Shapeshift from './Shapeshift';
import SMSVerification from './SMSVerification';
import Transfer from './Transfer'; import Transfer from './Transfer';
import PasswordManager from './PasswordManager'; import PasswordManager from './PasswordManager';
import SaveContract from './SaveContract'; import SaveContract from './SaveContract';
@ -36,6 +37,7 @@ export {
ExecuteContract, ExecuteContract,
FirstRun, FirstRun,
Shapeshift, Shapeshift,
SMSVerification,
Transfer, Transfer,
PasswordManager, PasswordManager,
LoadContract, LoadContract,

View File

@ -19,12 +19,13 @@ import Api from './api';
const sysuiToken = window.localStorage.getItem('sysuiToken'); const sysuiToken = window.localStorage.getItem('sysuiToken');
export default class SecureApi extends Api { export default class SecureApi extends Api {
constructor (url) { constructor (url, nextToken) {
super(new Api.Transport.Ws(url, sysuiToken)); super(new Api.Transport.Ws(url, sysuiToken));
this._isConnecting = true; this._isConnecting = true;
this._connectState = sysuiToken === 'initial' ? 1 : 0; this._connectState = sysuiToken === 'initial' ? 1 : 0;
this._needsToken = false; this._needsToken = false;
this._nextToken = nextToken;
this._dappsPort = 8080; this._dappsPort = 8080;
this._dappsInterface = null; this._dappsInterface = null;
this._signerPort = 8180; this._signerPort = 8180;
@ -57,7 +58,11 @@ export default class SecureApi extends Api {
if (isConnected) { if (isConnected) {
return this.connectSuccess(); return this.connectSuccess();
} else if (lastError) { } else if (lastError) {
this.updateToken('initial', 1); const nextToken = this._nextToken || 'initial';
const nextState = this._nextToken ? 0 : 1;
this._nextToken = null;
this.updateToken(nextToken, nextState);
} }
break; break;

Some files were not shown because too many files have changed in this diff Show More