Merge branch 'master' into auth-bft

This commit is contained in:
keorn 2016-12-10 11:01:23 +01:00
commit e5f8044cad
160 changed files with 3773 additions and 1341 deletions

View File

@ -422,13 +422,14 @@ test-rust-stable:
image: ethcore/rust:stable image: ethcore/rust:stable
before_script: before_script:
- git submodule update --init --recursive - git submodule update --init --recursive
- export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep \.js | wc -l) - export RUST_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep -v ^js/ | wc -l)
- echo $JS_FILES_MODIFIED - export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep ^js/ | wc -l)
- if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"; else ./js/scripts/install-deps.sh;fi - echo "rust/js modified: $RUST_FILES_MODIFIED / $JS_FILES_MODIFIED"
- if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS deps install since no JS files modified."; else ./js/scripts/install-deps.sh;fi
script: script:
- export RUST_BACKTRACE=1 - export RUST_BACKTRACE=1
- echo $JS_FILES_MODIFIED - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS lint since no JS files modified."; else ./js/scripts/lint.sh && ./js/scripts/test.sh && ./js/scripts/build.sh; fi
- if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"&./test.sh $CARGOFLAGS; else echo "skip rust test"&./js/scripts/lint.sh&./js/scripts/test.sh&./js/scripts/build.sh; fi - if [ "$RUST_FILES_MODIFIED" = 0 ]; then echo "Skipping Rust tests since no Rust files modified."; else ./test.sh $CARGOFLAGS; fi
tags: tags:
- rust - rust
- rust-stable - rust-stable
@ -437,13 +438,13 @@ js-test:
image: ethcore/rust:stable image: ethcore/rust:stable
before_script: before_script:
- git submodule update --init --recursive - git submodule update --init --recursive
- export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep \.js | wc -l) - export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep ^js/ | wc -l)
- echo $JS_FILES_MODIFIED - echo $JS_FILES_MODIFIED
- if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"; else ./js/scripts/install-deps.sh;fi - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS deps install since no JS files modified."; else ./js/scripts/install-deps.sh;fi
script: script:
- export RUST_BACKTRACE=1 - export RUST_BACKTRACE=1
- echo $JS_FILES_MODIFIED - echo $JS_FILES_MODIFIED
- if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js test"; else echo "skip rust test"&./js/scripts/lint.sh&./js/scripts/test.sh&./js/scripts/build.sh; fi - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS lint since no JS files modified."; else ./js/scripts/lint.sh && ./js/scripts/test.sh && ./js/scripts/build.sh; fi
tags: tags:
- rust - rust
- rust-stable - rust-stable
@ -484,11 +485,11 @@ js-release:
- stable - stable
image: ethcore/rust:stable image: ethcore/rust:stable
before_script: before_script:
- export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep \.js | wc -l) - export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep ^js/ | wc -l)
- echo $JS_FILES_MODIFIED - echo $JS_FILES_MODIFIED
- if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js build"; else ./js/scripts/install-deps.sh;fi - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS deps install since no JS files modified."; else ./js/scripts/install-deps.sh;fi
script: script:
- echo $JS_FILES_MODIFIED - echo $JS_FILES_MODIFIED
- if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "skip js build"; else ./js/scripts/build.sh&&./js/scripts/release.sh; fi - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS rebuild since no JS files modified."; else ./js/scripts/build.sh && ./js/scripts/release.sh; fi
tags: tags:
- javascript - javascript

19
Cargo.lock generated
View File

@ -18,6 +18,7 @@ dependencies = [
"ethcore-ipc-hypervisor 1.2.0", "ethcore-ipc-hypervisor 1.2.0",
"ethcore-ipc-nano 1.5.0", "ethcore-ipc-nano 1.5.0",
"ethcore-ipc-tests 0.1.0", "ethcore-ipc-tests 0.1.0",
"ethcore-light 1.5.0",
"ethcore-logger 1.5.0", "ethcore-logger 1.5.0",
"ethcore-rpc 1.5.0", "ethcore-rpc 1.5.0",
"ethcore-signer 1.5.0", "ethcore-signer 1.5.0",
@ -456,6 +457,21 @@ dependencies = [
"semver 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "ethcore-light"
version = "1.5.0"
dependencies = [
"ethcore 1.5.0",
"ethcore-io 1.5.0",
"ethcore-ipc 1.5.0",
"ethcore-ipc-codegen 1.5.0",
"ethcore-network 1.5.0",
"ethcore-util 1.5.0",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"rlp 0.1.0",
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "ethcore-logger" name = "ethcore-logger"
version = "1.5.0" version = "1.5.0"
@ -665,6 +681,7 @@ dependencies = [
"ethcore-ipc 1.5.0", "ethcore-ipc 1.5.0",
"ethcore-ipc-codegen 1.5.0", "ethcore-ipc-codegen 1.5.0",
"ethcore-ipc-nano 1.5.0", "ethcore-ipc-nano 1.5.0",
"ethcore-light 1.5.0",
"ethcore-network 1.5.0", "ethcore-network 1.5.0",
"ethcore-util 1.5.0", "ethcore-util 1.5.0",
"ethkey 0.2.0", "ethkey 0.2.0",
@ -1273,7 +1290,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#a59b62ecec8773715d1db7e070bbbe5443eb7378" source = "git+https://github.com/ethcore/js-precompiled.git#3d3b2f9e8e8b0fd62c172240bfd001a317cf2979"
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)",
] ]

View File

@ -47,6 +47,7 @@ rlp = { path = "util/rlp" }
ethcore-stratum = { path = "stratum" } ethcore-stratum = { path = "stratum" }
ethcore-dapps = { path = "dapps", optional = true } ethcore-dapps = { path = "dapps", optional = true }
clippy = { version = "0.0.103", optional = true} clippy = { version = "0.0.103", optional = true}
ethcore-light = { path = "ethcore/light" }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winapi = "0.2" winapi = "0.2"

View File

@ -8,7 +8,7 @@ authors = ["Ethcore <admin@ethcore.io>"]
build = "build.rs" build = "build.rs"
[build-dependencies] [build-dependencies]
"ethcore-ipc-codegen" = { path = "../../ipc/codegen" } "ethcore-ipc-codegen" = { path = "../../ipc/codegen", optional = true }
[dependencies] [dependencies]
log = "0.3" log = "0.3"
@ -16,6 +16,10 @@ ethcore = { path = ".." }
ethcore-util = { path = "../../util" } ethcore-util = { path = "../../util" }
ethcore-network = { path = "../../util/network" } ethcore-network = { path = "../../util/network" }
ethcore-io = { path = "../../util/io" } ethcore-io = { path = "../../util/io" }
ethcore-ipc = { path = "../../ipc/rpc" } ethcore-ipc = { path = "../../ipc/rpc", optional = true }
rlp = { path = "../../util/rlp" } rlp = { path = "../../util/rlp" }
time = "0.1" time = "0.1"
[features]
default = []
ipc = ["ethcore-ipc", "ethcore-ipc-codegen"]

View File

@ -14,8 +14,14 @@
// 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/>.
#[cfg(feature = "ipc")]
extern crate ethcore_ipc_codegen; extern crate ethcore_ipc_codegen;
#[cfg(feature = "ipc")]
fn main() { fn main() {
ethcore_ipc_codegen::derive_binary("src/types/mod.rs.in").unwrap(); ethcore_ipc_codegen::derive_binary("src/types/mod.rs.in").unwrap();
ethcore_ipc_codegen::derive_ipc_cond("src/provider.rs", true).unwrap();
} }
#[cfg(not(feature = "ipc"))]
fn main() { }

View File

@ -33,8 +33,21 @@
pub mod client; pub mod client;
pub mod net; pub mod net;
#[cfg(not(feature = "ipc"))]
pub mod provider; pub mod provider;
#[cfg(feature = "ipc")]
pub mod provider {
#![allow(dead_code, unused_assignments, unused_variables, missing_docs)] // codegen issues
include!(concat!(env!("OUT_DIR"), "/provider.rs"));
}
#[cfg(feature = "ipc")]
pub mod remote {
pub use provider::LightProviderClient;
}
mod types; mod types;
pub use self::provider::Provider; pub use self::provider::Provider;
@ -47,6 +60,8 @@ extern crate ethcore;
extern crate ethcore_util as util; extern crate ethcore_util as util;
extern crate ethcore_network as network; extern crate ethcore_network as network;
extern crate ethcore_io as io; extern crate ethcore_io as io;
extern crate ethcore_ipc as ipc;
extern crate rlp; extern crate rlp;
extern crate time; extern crate time;
#[cfg(feature = "ipc")]
extern crate ethcore_ipc as ipc;

View File

@ -22,6 +22,9 @@
//! //!
//! This module provides an interface for configuration of buffer //! This module provides an interface for configuration of buffer
//! flow costs and recharge rates. //! flow costs and recharge rates.
//!
//! Current default costs are picked completely arbitrarily, not based
//! on any empirical timings or mathematical models.
use request; use request;
use super::packet; use super::packet;
@ -273,6 +276,16 @@ impl FlowParams {
} }
} }
impl Default for FlowParams {
fn default() -> Self {
FlowParams {
limit: 50_000_000.into(),
costs: CostTable::default(),
recharge: 100_000.into(),
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -0,0 +1,120 @@
// 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/>.
//! I/O and event context generalizations.
use network::{NetworkContext, PeerId};
use super::{Announcement, LightProtocol, ReqId};
use super::error::Error;
use request::Request;
/// An I/O context which allows sending and receiving packets as well as
/// disconnecting peers. This is used as a generalization of the portions
/// of a p2p network which the light protocol structure makes use of.
pub trait IoContext {
/// Send a packet to a specific peer.
fn send(&self, peer: PeerId, packet_id: u8, packet_body: Vec<u8>);
/// Respond to a peer's message. Only works if this context is a byproduct
/// of a packet handler.
fn respond(&self, packet_id: u8, packet_body: Vec<u8>);
/// Disconnect a peer.
fn disconnect_peer(&self, peer: PeerId);
/// Disable a peer -- this is a disconnect + a time-out.
fn disable_peer(&self, peer: PeerId);
/// Get a peer's protocol version.
fn protocol_version(&self, peer: PeerId) -> Option<u8>;
}
impl<'a> IoContext for NetworkContext<'a> {
fn send(&self, peer: PeerId, packet_id: u8, packet_body: Vec<u8>) {
if let Err(e) = self.send(peer, packet_id, packet_body) {
debug!(target: "les", "Error sending packet to peer {}: {}", peer, e);
}
}
fn respond(&self, packet_id: u8, packet_body: Vec<u8>) {
if let Err(e) = self.respond(packet_id, packet_body) {
debug!(target: "les", "Error responding to peer message: {}", e);
}
}
fn disconnect_peer(&self, peer: PeerId) {
NetworkContext::disconnect_peer(self, peer);
}
fn disable_peer(&self, peer: PeerId) {
NetworkContext::disable_peer(self, peer);
}
fn protocol_version(&self, peer: PeerId) -> Option<u8> {
self.protocol_version(self.subprotocol_name(), peer)
}
}
/// Context for a protocol event.
pub trait EventContext {
/// Get the peer relevant to the event e.g. message sender,
/// disconnected/connected peer.
fn peer(&self) -> PeerId;
/// Make a request from a peer.
fn request_from(&self, peer: PeerId, request: Request) -> Result<ReqId, Error>;
/// Make an announcement of new capabilities to the rest of the peers.
// TODO: maybe just put this on a timer in LightProtocol?
fn make_announcement(&self, announcement: Announcement);
/// Disconnect a peer.
fn disconnect_peer(&self, peer: PeerId);
/// Disable a peer.
fn disable_peer(&self, peer: PeerId);
}
/// Concrete implementation of `EventContext` over the light protocol struct and
/// an io context.
pub struct Ctx<'a> {
/// Io context to enable immediate response to events.
pub io: &'a IoContext,
/// Protocol implementation.
pub proto: &'a LightProtocol,
/// Relevant peer for event.
pub peer: PeerId,
}
impl<'a> EventContext for Ctx<'a> {
fn peer(&self) -> PeerId { self.peer }
fn request_from(&self, peer: PeerId, request: Request) -> Result<ReqId, Error> {
self.proto.request_from(self.io, &peer, request)
}
fn make_announcement(&self, announcement: Announcement) {
self.proto.make_announcement(self.io, announcement);
}
fn disconnect_peer(&self, peer: PeerId) {
self.io.disconnect_peer(peer);
}
fn disable_peer(&self, peer: PeerId) {
self.io.disable_peer(peer);
}
}

View File

@ -54,6 +54,14 @@ pub enum Error {
WrongNetwork, WrongNetwork,
/// Unknown peer. /// Unknown peer.
UnknownPeer, UnknownPeer,
/// Unsolicited response.
UnsolicitedResponse,
/// Not a server.
NotServer,
/// Unsupported protocol version.
UnsupportedProtocolVersion(u8),
/// Bad protocol version.
BadProtocolVersion,
} }
impl Error { impl Error {
@ -67,6 +75,10 @@ impl Error {
Error::UnexpectedHandshake => Punishment::Disconnect, Error::UnexpectedHandshake => Punishment::Disconnect,
Error::WrongNetwork => Punishment::Disable, Error::WrongNetwork => Punishment::Disable,
Error::UnknownPeer => Punishment::Disconnect, Error::UnknownPeer => Punishment::Disconnect,
Error::UnsolicitedResponse => Punishment::Disable,
Error::NotServer => Punishment::Disable,
Error::UnsupportedProtocolVersion(_) => Punishment::Disable,
Error::BadProtocolVersion => Punishment::Disable,
} }
} }
} }
@ -92,7 +104,11 @@ impl fmt::Display for Error {
Error::UnrecognizedPacket(code) => write!(f, "Unrecognized packet: 0x{:x}", code), Error::UnrecognizedPacket(code) => write!(f, "Unrecognized packet: 0x{:x}", code),
Error::UnexpectedHandshake => write!(f, "Unexpected handshake"), Error::UnexpectedHandshake => write!(f, "Unexpected handshake"),
Error::WrongNetwork => write!(f, "Wrong network"), Error::WrongNetwork => write!(f, "Wrong network"),
Error::UnknownPeer => write!(f, "unknown peer"), Error::UnknownPeer => write!(f, "Unknown peer"),
Error::UnsolicitedResponse => write!(f, "Peer provided unsolicited data"),
Error::NotServer => write!(f, "Peer not a server."),
Error::UnsupportedProtocolVersion(pv) => write!(f, "Unsupported protocol version: {}", pv),
Error::BadProtocolVersion => write!(f, "Bad protocol version in handshake"),
} }
} }
} }

View File

@ -20,36 +20,51 @@
//! See https://github.com/ethcore/parity/wiki/Light-Ethereum-Subprotocol-(LES) //! See https://github.com/ethcore/parity/wiki/Light-Ethereum-Subprotocol-(LES)
use ethcore::transaction::SignedTransaction; use ethcore::transaction::SignedTransaction;
use ethcore::receipt::Receipt;
use io::TimerToken; use io::TimerToken;
use network::{NetworkProtocolHandler, NetworkContext, NetworkError, PeerId}; use network::{NetworkProtocolHandler, NetworkContext, PeerId};
use rlp::{RlpStream, Stream, UntrustedRlp, View}; use rlp::{RlpStream, Stream, UntrustedRlp, View};
use util::hash::H256; use util::hash::H256;
use util::{Mutex, RwLock, U256}; use util::{Bytes, Mutex, RwLock, U256};
use time::SteadyTime; use time::{Duration, SteadyTime};
use std::collections::{HashMap, HashSet}; use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use provider::Provider; use provider::Provider;
use request::{self, Request}; use request::{self, Request};
use self::buffer_flow::{Buffer, FlowParams}; use self::buffer_flow::{Buffer, FlowParams};
use self::context::Ctx;
use self::error::{Error, Punishment}; use self::error::{Error, Punishment};
mod buffer_flow; mod buffer_flow;
mod context;
mod error; mod error;
mod status; mod status;
pub use self::status::{Status, Capabilities, Announcement, NetworkId}; #[cfg(test)]
mod tests;
pub use self::context::{EventContext, IoContext};
pub use self::status::{Status, Capabilities, Announcement};
const TIMEOUT: TimerToken = 0; const TIMEOUT: TimerToken = 0;
const TIMEOUT_INTERVAL_MS: u64 = 1000; const TIMEOUT_INTERVAL_MS: u64 = 1000;
// LPV1 // minimum interval between updates.
const PROTOCOL_VERSION: u32 = 1; const UPDATE_INTERVAL_MS: i64 = 5000;
// TODO [rob] make configurable. // Supported protocol versions.
const PROTOCOL_ID: [u8; 3] = *b"les"; pub const PROTOCOL_VERSIONS: &'static [u8] = &[1];
// Max protocol version.
pub const MAX_PROTOCOL_VERSION: u8 = 1;
// Packet count for LES.
pub const PACKET_COUNT: u8 = 15;
// packet ID definitions. // packet ID definitions.
mod packet { mod packet {
@ -95,17 +110,19 @@ pub struct ReqId(usize);
// may not have received one for. // may not have received one for.
struct PendingPeer { struct PendingPeer {
sent_head: H256, sent_head: H256,
last_update: SteadyTime,
proto_version: u8,
} }
// data about each peer. // data about each peer.
struct Peer { struct Peer {
local_buffer: Buffer, // their buffer relative to us local_buffer: Buffer, // their buffer relative to us
remote_buffer: Buffer, // our buffer relative to them
current_asking: HashSet<usize>, // pending request ids.
status: Status, status: Status,
capabilities: Capabilities, capabilities: Capabilities,
remote_flow: FlowParams, remote_flow: Option<(Buffer, FlowParams)>,
sent_head: H256, // last head we've given them. sent_head: H256, // last head we've given them.
last_update: SteadyTime,
proto_version: u8,
} }
impl Peer { impl Peer {
@ -126,38 +143,56 @@ impl Peer {
self.local_buffer.current() self.local_buffer.current()
} }
// recharge remote buffer with remote flow params.
fn recharge_remote(&mut self) {
let flow = &mut self.remote_flow;
flow.recharge(&mut self.remote_buffer);
}
} }
/// An LES event handler. /// An LES event handler.
///
/// Each handler function takes a context which describes the relevant peer
/// and gives references to the IO layer and protocol structure so new messages
/// can be dispatched immediately.
///
/// Request responses are not guaranteed to be complete or valid, but passed IDs will be correct.
/// Response handlers are not given a copy of the original request; it is assumed
/// that relevant data will be stored by interested handlers.
pub trait Handler: Send + Sync { pub trait Handler: Send + Sync {
/// Called when a peer connects. /// Called when a peer connects.
fn on_connect(&self, _id: PeerId, _status: &Status, _capabilities: &Capabilities) { } fn on_connect(&self, _ctx: &EventContext, _status: &Status, _capabilities: &Capabilities) { }
/// Called when a peer disconnects /// Called when a peer disconnects, with a list of unfulfilled request IDs as
fn on_disconnect(&self, _id: PeerId) { } /// of yet.
fn on_disconnect(&self, _ctx: &EventContext, _unfulfilled: &[ReqId]) { }
/// Called when a peer makes an announcement. /// Called when a peer makes an announcement.
fn on_announcement(&self, _id: PeerId, _announcement: &Announcement) { } fn on_announcement(&self, _ctx: &EventContext, _announcement: &Announcement) { }
/// Called when a peer requests relay of some transactions. /// Called when a peer requests relay of some transactions.
fn on_transactions(&self, _id: PeerId, _relay: &[SignedTransaction]) { } fn on_transactions(&self, _ctx: &EventContext, _relay: &[SignedTransaction]) { }
/// Called when a peer responds with block bodies.
fn on_block_bodies(&self, _ctx: &EventContext, _req_id: ReqId, _bodies: &[Bytes]) { }
/// Called when a peer responds with block headers.
fn on_block_headers(&self, _ctx: &EventContext, _req_id: ReqId, _headers: &[Bytes]) { }
/// Called when a peer responds with block receipts.
fn on_receipts(&self, _ctx: &EventContext, _req_id: ReqId, _receipts: &[Vec<Receipt>]) { }
/// Called when a peer responds with state proofs. Each proof is a series of trie
/// nodes in ascending order by distance from the root.
fn on_state_proofs(&self, _ctx: &EventContext, _req_id: ReqId, _proofs: &[Vec<Bytes>]) { }
/// Called when a peer responds with contract code.
fn on_code(&self, _ctx: &EventContext, _req_id: ReqId, _codes: &[Bytes]) { }
/// Called when a peer responds with header proofs. Each proof is a block header coupled
/// with a series of trie nodes is ascending order by distance from the root.
fn on_header_proofs(&self, _ctx: &EventContext, _req_id: ReqId, _proofs: &[(Bytes, Vec<Bytes>)]) { }
/// Called on abort.
fn on_abort(&self) { }
} }
// a request and the time it was made. // a request, the peer who it was made to, and the time it was made.
struct Requested { struct Requested {
request: Request, request: Request,
timestamp: SteadyTime, timestamp: SteadyTime,
peer_id: PeerId,
} }
/// Protocol parameters. /// Protocol parameters.
pub struct Params { pub struct Params {
/// Genesis hash.
pub genesis_hash: H256,
/// Network id. /// Network id.
pub network_id: NetworkId, pub network_id: u64,
/// Buffer flow parameters. /// Buffer flow parameters.
pub flow_params: FlowParams, pub flow_params: FlowParams,
/// Initial capabilities. /// Initial capabilities.
@ -175,9 +210,9 @@ pub struct Params {
// Locks must be acquired in the order declared, and when holding a read lock // Locks must be acquired in the order declared, and when holding a read lock
// on the peers, only one peer may be held at a time. // on the peers, only one peer may be held at a time.
pub struct LightProtocol { pub struct LightProtocol {
provider: Box<Provider>, provider: Arc<Provider>,
genesis_hash: H256, genesis_hash: H256,
network_id: NetworkId, network_id: u64,
pending_peers: RwLock<HashMap<PeerId, PendingPeer>>, pending_peers: RwLock<HashMap<PeerId, PendingPeer>>,
peers: RwLock<HashMap<PeerId, Mutex<Peer>>>, peers: RwLock<HashMap<PeerId, Mutex<Peer>>>,
pending_requests: RwLock<HashMap<usize, Requested>>, pending_requests: RwLock<HashMap<usize, Requested>>,
@ -189,10 +224,13 @@ pub struct LightProtocol {
impl LightProtocol { impl LightProtocol {
/// Create a new instance of the protocol manager. /// Create a new instance of the protocol manager.
pub fn new(provider: Box<Provider>, params: Params) -> Self { pub fn new(provider: Arc<Provider>, params: Params) -> Self {
debug!(target: "les", "Initializing LES handler");
let genesis_hash = provider.chain_info().genesis_hash;
LightProtocol { LightProtocol {
provider: provider, provider: provider,
genesis_hash: params.genesis_hash, genesis_hash: genesis_hash,
network_id: params.network_id, network_id: params.network_id,
pending_peers: RwLock::new(HashMap::new()), pending_peers: RwLock::new(HashMap::new()),
peers: RwLock::new(HashMap::new()), peers: RwLock::new(HashMap::new()),
@ -207,28 +245,37 @@ impl LightProtocol {
/// Check the maximum amount of requests of a specific type /// Check the maximum amount of requests of a specific type
/// which a peer would be able to serve. /// which a peer would be able to serve.
pub fn max_requests(&self, peer: PeerId, kind: request::Kind) -> Option<usize> { pub fn max_requests(&self, peer: PeerId, kind: request::Kind) -> Option<usize> {
self.peers.read().get(&peer).map(|peer| { self.peers.read().get(&peer).and_then(|peer| {
let mut peer = peer.lock(); let mut peer = peer.lock();
peer.recharge_remote(); match peer.remote_flow.as_mut() {
peer.remote_flow.max_amount(&peer.remote_buffer, kind) Some(&mut (ref mut buf, ref flow)) => {
flow.recharge(buf);
Some(flow.max_amount(&*buf, kind))
}
None => None,
}
}) })
} }
/// Make a request to a peer. /// Make a request to a peer.
/// ///
/// Fails on: nonexistent peer, network error, /// Fails on: nonexistent peer, network error, peer not server,
/// insufficient buffer. Does not check capabilities before sending. /// insufficient buffer. Does not check capabilities before sending.
/// On success, returns a request id which can later be coordinated /// On success, returns a request id which can later be coordinated
/// with an event. /// with an event.
pub fn request_from(&self, io: &NetworkContext, peer_id: &PeerId, request: Request) -> Result<ReqId, Error> { pub fn request_from(&self, io: &IoContext, peer_id: &PeerId, request: Request) -> Result<ReqId, Error> {
let peers = self.peers.read(); let peers = self.peers.read();
let peer = try!(peers.get(peer_id).ok_or_else(|| Error::UnknownPeer)); let peer = try!(peers.get(peer_id).ok_or_else(|| Error::UnknownPeer));
let mut peer = peer.lock(); let mut peer = peer.lock();
peer.recharge_remote(); match peer.remote_flow.as_mut() {
Some(&mut (ref mut buf, ref flow)) => {
let max = peer.remote_flow.compute_cost(request.kind(), request.amount()); flow.recharge(buf);
try!(peer.remote_buffer.deduct_cost(max)); let max = flow.compute_cost(request.kind(), request.amount());
try!(buf.deduct_cost(max));
}
None => return Err(Error::NotServer),
}
let req_id = self.req_id.fetch_add(1, Ordering::SeqCst); let req_id = self.req_id.fetch_add(1, Ordering::SeqCst);
let packet_data = encode_request(&request, req_id); let packet_data = encode_request(&request, req_id);
@ -242,12 +289,12 @@ impl LightProtocol {
request::Kind::HeaderProofs => packet::GET_HEADER_PROOFS, request::Kind::HeaderProofs => packet::GET_HEADER_PROOFS,
}; };
try!(io.send(*peer_id, packet_id, packet_data)); io.send(*peer_id, packet_id, packet_data);
peer.current_asking.insert(req_id);
self.pending_requests.write().insert(req_id, Requested { self.pending_requests.write().insert(req_id, Requested {
request: request, request: request,
timestamp: SteadyTime::now(), timestamp: SteadyTime::now(),
peer_id: *peer_id,
}); });
Ok(ReqId(req_id)) Ok(ReqId(req_id))
@ -255,8 +302,9 @@ impl LightProtocol {
/// Make an announcement of new chain head and capabilities to all peers. /// Make an announcement of new chain head and capabilities to all peers.
/// The announcement is expected to be valid. /// The announcement is expected to be valid.
pub fn make_announcement(&self, io: &NetworkContext, mut announcement: Announcement) { pub fn make_announcement(&self, io: &IoContext, mut announcement: Announcement) {
let mut reorgs_map = HashMap::new(); let mut reorgs_map = HashMap::new();
let now = SteadyTime::now();
// update stored capabilities // update stored capabilities
self.capabilities.write().update_from(&announcement); self.capabilities.write().update_from(&announcement);
@ -264,6 +312,17 @@ impl LightProtocol {
// calculate reorg info and send packets // calculate reorg info and send packets
for (peer_id, peer_info) in self.peers.read().iter() { for (peer_id, peer_info) in self.peers.read().iter() {
let mut peer_info = peer_info.lock(); let mut peer_info = peer_info.lock();
// TODO: "urgent" announcements like new blocks?
// the timer approach will skip 1 (possibly 2) in rare occasions.
if peer_info.sent_head == announcement.head_hash ||
peer_info.status.head_num >= announcement.head_num ||
now - peer_info.last_update < Duration::milliseconds(UPDATE_INTERVAL_MS) {
continue
}
peer_info.last_update = now;
let reorg_depth = reorgs_map.entry(peer_info.sent_head) let reorg_depth = reorgs_map.entry(peer_info.sent_head)
.or_insert_with(|| { .or_insert_with(|| {
match self.provider.reorg_depth(&announcement.head_hash, &peer_info.sent_head) { match self.provider.reorg_depth(&announcement.head_hash, &peer_info.sent_head) {
@ -281,26 +340,133 @@ impl LightProtocol {
peer_info.sent_head = announcement.head_hash; peer_info.sent_head = announcement.head_hash;
announcement.reorg_depth = *reorg_depth; announcement.reorg_depth = *reorg_depth;
if let Err(e) = io.send(*peer_id, packet::ANNOUNCE, status::write_announcement(&announcement)) { io.send(*peer_id, packet::ANNOUNCE, status::write_announcement(&announcement));
debug!(target: "les", "Error sending to peer {}: {}", peer_id, e);
}
} }
} }
/// Add an event handler. /// Add an event handler.
/// Ownership will be transferred to the protocol structure, /// Ownership will be transferred to the protocol structure,
/// and the handler will be kept alive as long as it is. /// and the handler will be kept alive as long as it is.
/// These are intended to be added at the beginning of the /// These are intended to be added when the protocol structure
/// is initialized as a means of customizing its behavior.
pub fn add_handler(&mut self, handler: Box<Handler>) { pub fn add_handler(&mut self, handler: Box<Handler>) {
self.handlers.push(handler); self.handlers.push(handler);
} }
/// Signal to handlers that network activity is being aborted
/// and clear peer data.
pub fn abort(&self) {
for handler in &self.handlers {
handler.on_abort();
}
// acquire in order and hold.
let mut pending_peers = self.pending_peers.write();
let mut peers = self.peers.write();
let mut pending_requests = self.pending_requests.write();
pending_peers.clear();
peers.clear();
pending_requests.clear();
}
// Does the common pre-verification of responses before the response itself
// is actually decoded:
// - check whether peer exists
// - check whether request was made
// - check whether request kinds match
fn pre_verify_response(&self, peer: &PeerId, kind: request::Kind, raw: &UntrustedRlp) -> Result<ReqId, Error> {
let req_id: usize = try!(raw.val_at(0));
let cur_buffer: U256 = try!(raw.val_at(1));
trace!(target: "les", "pre-verifying response from peer {}, kind={:?}", peer, kind);
match self.pending_requests.write().remove(&req_id) {
None => return Err(Error::UnsolicitedResponse),
Some(requested) => {
if requested.peer_id != *peer || requested.request.kind() != kind {
return Err(Error::UnsolicitedResponse)
}
}
}
let peers = self.peers.read();
match peers.get(peer) {
Some(peer_info) => {
let mut peer_info = peer_info.lock();
match peer_info.remote_flow.as_mut() {
Some(&mut (ref mut buf, ref mut flow)) => {
let actual_buffer = ::std::cmp::min(cur_buffer, *flow.limit());
buf.update_to(actual_buffer)
}
None => return Err(Error::NotServer), // this really should be impossible.
}
Ok(ReqId(req_id))
}
None => Err(Error::UnknownPeer), // probably only occurs in a race of some kind.
}
}
// handle a packet using the given io context.
fn handle_packet(&self, io: &IoContext, peer: &PeerId, packet_id: u8, data: &[u8]) {
let rlp = UntrustedRlp::new(data);
trace!(target: "les", "Incoming packet {} from peer {}", packet_id, peer);
// handle the packet
let res = match packet_id {
packet::STATUS => self.status(peer, io, rlp),
packet::ANNOUNCE => self.announcement(peer, io, rlp),
packet::GET_BLOCK_HEADERS => self.get_block_headers(peer, io, rlp),
packet::BLOCK_HEADERS => self.block_headers(peer, io, rlp),
packet::GET_BLOCK_BODIES => self.get_block_bodies(peer, io, rlp),
packet::BLOCK_BODIES => self.block_bodies(peer, io, rlp),
packet::GET_RECEIPTS => self.get_receipts(peer, io, rlp),
packet::RECEIPTS => self.receipts(peer, io, rlp),
packet::GET_PROOFS => self.get_proofs(peer, io, rlp),
packet::PROOFS => self.proofs(peer, io, rlp),
packet::GET_CONTRACT_CODES => self.get_contract_code(peer, io, rlp),
packet::CONTRACT_CODES => self.contract_code(peer, io, rlp),
packet::GET_HEADER_PROOFS => self.get_header_proofs(peer, io, rlp),
packet::HEADER_PROOFS => self.header_proofs(peer, io, rlp),
packet::SEND_TRANSACTIONS => self.relay_transactions(peer, io, rlp),
other => {
Err(Error::UnrecognizedPacket(other))
}
};
// if something went wrong, figure out how much to punish the peer.
if let Err(e) = res {
match e.punishment() {
Punishment::None => {}
Punishment::Disconnect => {
debug!(target: "les", "Disconnecting peer {}: {}", peer, e);
io.disconnect_peer(*peer)
}
Punishment::Disable => {
debug!(target: "les", "Disabling peer {}: {}", peer, e);
io.disable_peer(*peer)
}
}
}
}
} }
impl LightProtocol { impl LightProtocol {
// called when a peer connects. // called when a peer connects.
fn on_connect(&self, peer: &PeerId, io: &NetworkContext) { fn on_connect(&self, peer: &PeerId, io: &IoContext) {
let peer = *peer; let peer = *peer;
trace!(target: "les", "Peer {} connecting", peer);
match self.send_status(peer, io) { match self.send_status(peer, io) {
Ok(pending_peer) => { Ok(pending_peer) => {
self.pending_peers.write().insert(peer, pending_peer); self.pending_peers.write().insert(peer, pending_peer);
@ -313,44 +479,69 @@ impl LightProtocol {
} }
// called when a peer disconnects. // called when a peer disconnects.
fn on_disconnect(&self, peer: PeerId) { fn on_disconnect(&self, peer: PeerId, io: &IoContext) {
// TODO: reassign all requests assigned to this peer. trace!(target: "les", "Peer {} disconnecting", peer);
self.pending_peers.write().remove(&peer); self.pending_peers.write().remove(&peer);
if self.peers.write().remove(&peer).is_some() { if self.peers.write().remove(&peer).is_some() {
let unfulfilled: Vec<_> = self.pending_requests.read()
.iter()
.filter(|&(_, r)| r.peer_id == peer)
.map(|(&id, _)| ReqId(id))
.collect();
{
let mut pending = self.pending_requests.write();
for &ReqId(ref inner) in &unfulfilled {
pending.remove(inner);
}
}
for handler in &self.handlers { for handler in &self.handlers {
handler.on_disconnect(peer) handler.on_disconnect(&Ctx {
peer: peer,
io: io,
proto: self,
}, &unfulfilled)
} }
} }
} }
// send status to a peer. // send status to a peer.
fn send_status(&self, peer: PeerId, io: &NetworkContext) -> Result<PendingPeer, NetworkError> { fn send_status(&self, peer: PeerId, io: &IoContext) -> Result<PendingPeer, Error> {
let chain_info = self.provider.chain_info(); let proto_version = try!(io.protocol_version(peer).ok_or(Error::WrongNetwork));
// TODO: could update capabilities here. if PROTOCOL_VERSIONS.iter().find(|x| **x == proto_version).is_none() {
return Err(Error::UnsupportedProtocolVersion(proto_version));
}
let chain_info = self.provider.chain_info();
let status = Status { let status = Status {
head_td: chain_info.total_difficulty, head_td: chain_info.total_difficulty,
head_hash: chain_info.best_block_hash, head_hash: chain_info.best_block_hash,
head_num: chain_info.best_block_number, head_num: chain_info.best_block_number,
genesis_hash: chain_info.genesis_hash, genesis_hash: chain_info.genesis_hash,
protocol_version: PROTOCOL_VERSION, protocol_version: proto_version as u32, // match peer proto version
network_id: self.network_id, network_id: self.network_id,
last_head: None, last_head: None,
}; };
let capabilities = self.capabilities.read().clone(); let capabilities = self.capabilities.read().clone();
let status_packet = status::write_handshake(&status, &capabilities, &self.flow_params); let status_packet = status::write_handshake(&status, &capabilities, Some(&self.flow_params));
try!(io.send(peer, packet::STATUS, status_packet)); io.send(peer, packet::STATUS, status_packet);
Ok(PendingPeer { Ok(PendingPeer {
sent_head: chain_info.best_block_hash, sent_head: chain_info.best_block_hash,
last_update: SteadyTime::now(),
proto_version: proto_version,
}) })
} }
// Handle status message from peer. // Handle status message from peer.
fn status(&self, peer: &PeerId, data: UntrustedRlp) -> Result<(), Error> { fn status(&self, peer: &PeerId, io: &IoContext, data: UntrustedRlp) -> Result<(), Error> {
let pending = match self.pending_peers.write().remove(peer) { let pending = match self.pending_peers.write().remove(peer) {
Some(pending) => pending, Some(pending) => pending,
None => { None => {
@ -366,33 +557,45 @@ impl LightProtocol {
return Err(Error::WrongNetwork); return Err(Error::WrongNetwork);
} }
if Some(status.protocol_version as u8) != io.protocol_version(*peer) {
return Err(Error::BadProtocolVersion);
}
let remote_flow = flow_params.map(|params| (params.create_buffer(), params));
self.peers.write().insert(*peer, Mutex::new(Peer { self.peers.write().insert(*peer, Mutex::new(Peer {
local_buffer: self.flow_params.create_buffer(), local_buffer: self.flow_params.create_buffer(),
remote_buffer: flow_params.create_buffer(),
current_asking: HashSet::new(),
status: status.clone(), status: status.clone(),
capabilities: capabilities.clone(), capabilities: capabilities.clone(),
remote_flow: flow_params, remote_flow: remote_flow,
sent_head: pending.sent_head, sent_head: pending.sent_head,
last_update: pending.last_update,
proto_version: pending.proto_version,
})); }));
for handler in &self.handlers { for handler in &self.handlers {
handler.on_connect(*peer, &status, &capabilities) handler.on_connect(&Ctx {
peer: *peer,
io: io,
proto: self,
}, &status, &capabilities)
} }
Ok(()) Ok(())
} }
// Handle an announcement. // Handle an announcement.
fn announcement(&self, peer: &PeerId, data: UntrustedRlp) -> Result<(), Error> { fn announcement(&self, peer: &PeerId, io: &IoContext, data: UntrustedRlp) -> Result<(), Error> {
if !self.peers.read().contains_key(peer) { if !self.peers.read().contains_key(peer) {
debug!(target: "les", "Ignoring announcement from unknown peer"); debug!(target: "les", "Ignoring announcement from unknown peer");
return Ok(()) return Ok(())
} }
let announcement = try!(status::parse_announcement(data)); let announcement = try!(status::parse_announcement(data));
let peers = self.peers.read();
// scope to ensure locks are dropped before moving into handler-space.
{
let peers = self.peers.read();
let peer_info = match peers.get(peer) { let peer_info = match peers.get(peer) {
Some(info) => info, Some(info) => info,
None => return Ok(()), None => return Ok(()),
@ -413,16 +616,21 @@ impl LightProtocol {
// update capabilities. // update capabilities.
peer_info.capabilities.update_from(&announcement); peer_info.capabilities.update_from(&announcement);
}
for handler in &self.handlers { for handler in &self.handlers {
handler.on_announcement(*peer, &announcement); handler.on_announcement(&Ctx {
peer: *peer,
io: io,
proto: self,
}, &announcement);
} }
Ok(()) Ok(())
} }
// Handle a request for block headers. // Handle a request for block headers.
fn get_block_headers(&self, peer: &PeerId, io: &NetworkContext, data: UntrustedRlp) -> Result<(), Error> { fn get_block_headers(&self, peer: &PeerId, io: &IoContext, data: UntrustedRlp) -> Result<(), Error> {
const MAX_HEADERS: usize = 512; const MAX_HEADERS: usize = 512;
let peers = self.peers.read(); let peers = self.peers.read();
@ -467,16 +675,29 @@ impl LightProtocol {
} }
stream.out() stream.out()
}).map_err(Into::into) });
Ok(())
} }
// Receive a response for block headers. // Receive a response for block headers.
fn block_headers(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { fn block_headers(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> {
unimplemented!() let req_id = try!(self.pre_verify_response(peer, request::Kind::Headers, &raw));
let raw_headers: Vec<_> = raw.iter().skip(2).map(|x| x.as_raw().to_owned()).collect();
for handler in &self.handlers {
handler.on_block_headers(&Ctx {
peer: *peer,
io: io,
proto: self,
}, req_id, &raw_headers);
}
Ok(())
} }
// Handle a request for block bodies. // Handle a request for block bodies.
fn get_block_bodies(&self, peer: &PeerId, io: &NetworkContext, data: UntrustedRlp) -> Result<(), Error> { fn get_block_bodies(&self, peer: &PeerId, io: &IoContext, data: UntrustedRlp) -> Result<(), Error> {
const MAX_BODIES: usize = 256; const MAX_BODIES: usize = 256;
let peers = self.peers.read(); let peers = self.peers.read();
@ -513,16 +734,29 @@ impl LightProtocol {
} }
stream.out() stream.out()
}).map_err(Into::into) });
Ok(())
} }
// Receive a response for block bodies. // Receive a response for block bodies.
fn block_bodies(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { fn block_bodies(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> {
unimplemented!() let req_id = try!(self.pre_verify_response(peer, request::Kind::Bodies, &raw));
let raw_bodies: Vec<Bytes> = raw.iter().skip(2).map(|x| x.as_raw().to_owned()).collect();
for handler in &self.handlers {
handler.on_block_bodies(&Ctx {
peer: *peer,
io: io,
proto: self,
}, req_id, &raw_bodies);
}
Ok(())
} }
// Handle a request for receipts. // Handle a request for receipts.
fn get_receipts(&self, peer: &PeerId, io: &NetworkContext, data: UntrustedRlp) -> Result<(), Error> { fn get_receipts(&self, peer: &PeerId, io: &IoContext, data: UntrustedRlp) -> Result<(), Error> {
const MAX_RECEIPTS: usize = 256; const MAX_RECEIPTS: usize = 256;
let peers = self.peers.read(); let peers = self.peers.read();
@ -559,16 +793,33 @@ impl LightProtocol {
} }
stream.out() stream.out()
}).map_err(Into::into) });
Ok(())
} }
// Receive a response for receipts. // Receive a response for receipts.
fn receipts(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { fn receipts(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> {
unimplemented!() let req_id = try!(self.pre_verify_response(peer, request::Kind::Receipts, &raw));
let raw_receipts: Vec<Vec<Receipt>> = try!(raw
.iter()
.skip(2)
.map(|x| x.as_val())
.collect());
for handler in &self.handlers {
handler.on_receipts(&Ctx {
peer: *peer,
io: io,
proto: self,
}, req_id, &raw_receipts);
}
Ok(())
} }
// Handle a request for proofs. // Handle a request for proofs.
fn get_proofs(&self, peer: &PeerId, io: &NetworkContext, data: UntrustedRlp) -> Result<(), Error> { fn get_proofs(&self, peer: &PeerId, io: &IoContext, data: UntrustedRlp) -> Result<(), Error> {
const MAX_PROOFS: usize = 128; const MAX_PROOFS: usize = 128;
let peers = self.peers.read(); let peers = self.peers.read();
@ -616,16 +867,33 @@ impl LightProtocol {
} }
stream.out() stream.out()
}).map_err(Into::into) });
Ok(())
} }
// Receive a response for proofs. // Receive a response for proofs.
fn proofs(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { fn proofs(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> {
unimplemented!() let req_id = try!(self.pre_verify_response(peer, request::Kind::StateProofs, &raw));
let raw_proofs: Vec<Vec<Bytes>> = raw.iter()
.skip(2)
.map(|x| x.iter().map(|node| node.as_raw().to_owned()).collect())
.collect();
for handler in &self.handlers {
handler.on_state_proofs(&Ctx {
peer: *peer,
io: io,
proto: self,
}, req_id, &raw_proofs);
}
Ok(())
} }
// Handle a request for contract code. // Handle a request for contract code.
fn get_contract_code(&self, peer: &PeerId, io: &NetworkContext, data: UntrustedRlp) -> Result<(), Error> { fn get_contract_code(&self, peer: &PeerId, io: &IoContext, data: UntrustedRlp) -> Result<(), Error> {
const MAX_CODES: usize = 256; const MAX_CODES: usize = 256;
let peers = self.peers.read(); let peers = self.peers.read();
@ -667,20 +935,34 @@ impl LightProtocol {
stream.append(&req_id).append(&cur_buffer); stream.append(&req_id).append(&cur_buffer);
for code in response { for code in response {
stream.append_raw(&code, 1); stream.append(&code);
} }
stream.out() stream.out()
}).map_err(Into::into) });
Ok(())
} }
// Receive a response for contract code. // Receive a response for contract code.
fn contract_code(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { fn contract_code(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> {
unimplemented!() let req_id = try!(self.pre_verify_response(peer, request::Kind::Codes, &raw));
let raw_code: Vec<Bytes> = try!(raw.iter().skip(2).map(|x| x.as_val()).collect());
for handler in &self.handlers {
handler.on_code(&Ctx {
peer: *peer,
io: io,
proto: self,
}, req_id, &raw_code);
}
Ok(())
} }
// Handle a request for header proofs // Handle a request for header proofs
fn get_header_proofs(&self, peer: &PeerId, io: &NetworkContext, data: UntrustedRlp) -> Result<(), Error> { fn get_header_proofs(&self, peer: &PeerId, io: &IoContext, data: UntrustedRlp) -> Result<(), Error> {
const MAX_PROOFS: usize = 256; const MAX_PROOFS: usize = 256;
let peers = self.peers.read(); let peers = self.peers.read();
@ -727,16 +1009,37 @@ impl LightProtocol {
} }
stream.out() stream.out()
}).map_err(Into::into) });
Ok(())
} }
// Receive a response for header proofs // Receive a response for header proofs
fn header_proofs(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> { fn header_proofs(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> {
unimplemented!() fn decode_res(raw: UntrustedRlp) -> Result<(Bytes, Vec<Bytes>), ::rlp::DecoderError> {
Ok((
try!(raw.val_at(0)),
try!(raw.at(1)).iter().map(|x| x.as_raw().to_owned()).collect(),
))
}
let req_id = try!(self.pre_verify_response(peer, request::Kind::HeaderProofs, &raw));
let raw_proofs: Vec<_> = try!(raw.iter().skip(2).map(decode_res).collect());
for handler in &self.handlers {
handler.on_header_proofs(&Ctx {
peer: *peer,
io: io,
proto: self,
}, req_id, &raw_proofs);
}
Ok(())
} }
// Receive a set of transactions to relay. // Receive a set of transactions to relay.
fn relay_transactions(&self, peer: &PeerId, data: UntrustedRlp) -> Result<(), Error> { fn relay_transactions(&self, peer: &PeerId, io: &IoContext, data: UntrustedRlp) -> Result<(), Error> {
const MAX_TRANSACTIONS: usize = 256; const MAX_TRANSACTIONS: usize = 256;
let txs: Vec<_> = try!(data.iter().take(MAX_TRANSACTIONS).map(|x| x.as_val::<SignedTransaction>()).collect()); let txs: Vec<_> = try!(data.iter().take(MAX_TRANSACTIONS).map(|x| x.as_val::<SignedTransaction>()).collect());
@ -744,7 +1047,11 @@ impl LightProtocol {
debug!(target: "les", "Received {} transactions to relay from peer {}", txs.len(), peer); debug!(target: "les", "Received {} transactions to relay from peer {}", txs.len(), peer);
for handler in &self.handlers { for handler in &self.handlers {
handler.on_transactions(*peer, &txs); handler.on_transactions(&Ctx {
peer: *peer,
io: io,
proto: self,
}, &txs);
} }
Ok(()) Ok(())
@ -757,60 +1064,15 @@ impl NetworkProtocolHandler for LightProtocol {
} }
fn read(&self, io: &NetworkContext, peer: &PeerId, packet_id: u8, data: &[u8]) { fn read(&self, io: &NetworkContext, peer: &PeerId, packet_id: u8, data: &[u8]) {
let rlp = UntrustedRlp::new(data); self.handle_packet(io, peer, packet_id, data);
// handle the packet
let res = match packet_id {
packet::STATUS => self.status(peer, rlp),
packet::ANNOUNCE => self.announcement(peer, rlp),
packet::GET_BLOCK_HEADERS => self.get_block_headers(peer, io, rlp),
packet::BLOCK_HEADERS => self.block_headers(peer, io, rlp),
packet::GET_BLOCK_BODIES => self.get_block_bodies(peer, io, rlp),
packet::BLOCK_BODIES => self.block_bodies(peer, io, rlp),
packet::GET_RECEIPTS => self.get_receipts(peer, io, rlp),
packet::RECEIPTS => self.receipts(peer, io, rlp),
packet::GET_PROOFS => self.get_proofs(peer, io, rlp),
packet::PROOFS => self.proofs(peer, io, rlp),
packet::GET_CONTRACT_CODES => self.get_contract_code(peer, io, rlp),
packet::CONTRACT_CODES => self.contract_code(peer, io, rlp),
packet::GET_HEADER_PROOFS => self.get_header_proofs(peer, io, rlp),
packet::HEADER_PROOFS => self.header_proofs(peer, io, rlp),
packet::SEND_TRANSACTIONS => self.relay_transactions(peer, rlp),
other => {
Err(Error::UnrecognizedPacket(other))
}
};
// if something went wrong, figure out how much to punish the peer.
if let Err(e) = res {
match e.punishment() {
Punishment::None => {}
Punishment::Disconnect => {
debug!(target: "les", "Disconnecting peer {}: {}", peer, e);
io.disconnect_peer(*peer)
}
Punishment::Disable => {
debug!(target: "les", "Disabling peer {}: {}", peer, e);
io.disable_peer(*peer)
}
}
}
} }
fn connected(&self, io: &NetworkContext, peer: &PeerId) { fn connected(&self, io: &NetworkContext, peer: &PeerId) {
self.on_connect(peer, io); self.on_connect(peer, io);
} }
fn disconnected(&self, _io: &NetworkContext, peer: &PeerId) { fn disconnected(&self, io: &NetworkContext, peer: &PeerId) {
self.on_disconnect(*peer); self.on_disconnect(*peer, io);
} }
fn timeout(&self, _io: &NetworkContext, timer: TimerToken) { fn timeout(&self, _io: &NetworkContext, timer: TimerToken) {

View File

@ -82,26 +82,6 @@ impl Key {
} }
} }
/// Network ID structure.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum NetworkId {
/// ID for the mainnet
Mainnet = 1,
/// ID for the testnet
Testnet = 0,
}
impl NetworkId {
fn from_raw(raw: u32) -> Option<Self> {
match raw {
0 => Some(NetworkId::Testnet),
1 => Some(NetworkId::Mainnet),
_ => None,
}
}
}
// helper for decoding key-value pairs in the handshake or an announcement. // helper for decoding key-value pairs in the handshake or an announcement.
struct Parser<'a> { struct Parser<'a> {
pos: usize, pos: usize,
@ -118,6 +98,7 @@ impl<'a> Parser<'a> {
// expect a specific next key, and get the value's RLP. // expect a specific next key, and get the value's RLP.
// if the key isn't found, the position isn't advanced. // if the key isn't found, the position isn't advanced.
fn expect_raw(&mut self, key: Key) -> Result<UntrustedRlp<'a>, DecoderError> { fn expect_raw(&mut self, key: Key) -> Result<UntrustedRlp<'a>, DecoderError> {
trace!(target: "les", "Expecting key {}", key.as_str());
let pre_pos = self.pos; let pre_pos = self.pos;
if let Some((k, val)) = try!(self.get_next()) { if let Some((k, val)) = try!(self.get_next()) {
if k == key { return Ok(val) } if k == key { return Ok(val) }
@ -164,7 +145,7 @@ pub struct Status {
/// Protocol version. /// Protocol version.
pub protocol_version: u32, pub protocol_version: u32,
/// Network id of this peer. /// Network id of this peer.
pub network_id: NetworkId, pub network_id: u64,
/// Total difficulty of the head of the chain. /// Total difficulty of the head of the chain.
pub head_td: U256, pub head_td: U256,
/// Hash of the best block. /// Hash of the best block.
@ -217,7 +198,7 @@ impl Capabilities {
/// - chain status /// - chain status
/// - serving capabilities /// - serving capabilities
/// - buffer flow parameters /// - buffer flow parameters
pub fn parse_handshake(rlp: UntrustedRlp) -> Result<(Status, Capabilities, FlowParams), DecoderError> { pub fn parse_handshake(rlp: UntrustedRlp) -> Result<(Status, Capabilities, Option<FlowParams>), DecoderError> {
let mut parser = Parser { let mut parser = Parser {
pos: 0, pos: 0,
rlp: rlp, rlp: rlp,
@ -225,8 +206,7 @@ pub fn parse_handshake(rlp: UntrustedRlp) -> Result<(Status, Capabilities, FlowP
let status = Status { let status = Status {
protocol_version: try!(parser.expect(Key::ProtocolVersion)), protocol_version: try!(parser.expect(Key::ProtocolVersion)),
network_id: try!(parser.expect(Key::NetworkId) network_id: try!(parser.expect(Key::NetworkId)),
.and_then(|id: u32| NetworkId::from_raw(id).ok_or(DecoderError::Custom("Invalid network ID")))),
head_td: try!(parser.expect(Key::HeadTD)), head_td: try!(parser.expect(Key::HeadTD)),
head_hash: try!(parser.expect(Key::HeadHash)), head_hash: try!(parser.expect(Key::HeadHash)),
head_num: try!(parser.expect(Key::HeadNum)), head_num: try!(parser.expect(Key::HeadNum)),
@ -241,20 +221,23 @@ pub fn parse_handshake(rlp: UntrustedRlp) -> Result<(Status, Capabilities, FlowP
tx_relay: parser.expect_raw(Key::TxRelay).is_ok(), tx_relay: parser.expect_raw(Key::TxRelay).is_ok(),
}; };
let flow_params = FlowParams::new( let flow_params = match (
try!(parser.expect(Key::BufferLimit)), parser.expect(Key::BufferLimit),
try!(parser.expect(Key::BufferCostTable)), parser.expect(Key::BufferCostTable),
try!(parser.expect(Key::BufferRechargeRate)), parser.expect(Key::BufferRechargeRate)
); ) {
(Ok(bl), Ok(bct), Ok(brr)) => Some(FlowParams::new(bl, bct, brr)),
_ => None,
};
Ok((status, capabilities, flow_params)) Ok((status, capabilities, flow_params))
} }
/// Write a handshake, given status, capabilities, and flow parameters. /// Write a handshake, given status, capabilities, and flow parameters.
pub fn write_handshake(status: &Status, capabilities: &Capabilities, flow_params: &FlowParams) -> Vec<u8> { pub fn write_handshake(status: &Status, capabilities: &Capabilities, flow_params: Option<&FlowParams>) -> Vec<u8> {
let mut pairs = Vec::new(); let mut pairs = Vec::new();
pairs.push(encode_pair(Key::ProtocolVersion, &status.protocol_version)); pairs.push(encode_pair(Key::ProtocolVersion, &status.protocol_version));
pairs.push(encode_pair(Key::NetworkId, &(status.network_id as u32))); pairs.push(encode_pair(Key::NetworkId, &(status.network_id as u64)));
pairs.push(encode_pair(Key::HeadTD, &status.head_td)); pairs.push(encode_pair(Key::HeadTD, &status.head_td));
pairs.push(encode_pair(Key::HeadHash, &status.head_hash)); pairs.push(encode_pair(Key::HeadHash, &status.head_hash));
pairs.push(encode_pair(Key::HeadNum, &status.head_num)); pairs.push(encode_pair(Key::HeadNum, &status.head_num));
@ -273,9 +256,11 @@ pub fn write_handshake(status: &Status, capabilities: &Capabilities, flow_params
pairs.push(encode_flag(Key::TxRelay)); pairs.push(encode_flag(Key::TxRelay));
} }
if let Some(flow_params) = flow_params {
pairs.push(encode_pair(Key::BufferLimit, flow_params.limit())); pairs.push(encode_pair(Key::BufferLimit, flow_params.limit()));
pairs.push(encode_pair(Key::BufferCostTable, flow_params.cost_table())); pairs.push(encode_pair(Key::BufferCostTable, flow_params.cost_table()));
pairs.push(encode_pair(Key::BufferRechargeRate, flow_params.recharge_rate())); pairs.push(encode_pair(Key::BufferRechargeRate, flow_params.recharge_rate()));
}
let mut stream = RlpStream::new_list(pairs.len()); let mut stream = RlpStream::new_list(pairs.len());
@ -385,7 +370,7 @@ mod tests {
fn full_handshake() { fn full_handshake() {
let status = Status { let status = Status {
protocol_version: 1, protocol_version: 1,
network_id: NetworkId::Mainnet, network_id: 1,
head_td: U256::default(), head_td: U256::default(),
head_hash: H256::default(), head_hash: H256::default(),
head_num: 10, head_num: 10,
@ -406,21 +391,21 @@ mod tests {
1000.into(), 1000.into(),
); );
let handshake = write_handshake(&status, &capabilities, &flow_params); let handshake = write_handshake(&status, &capabilities, Some(&flow_params));
let (read_status, read_capabilities, read_flow) let (read_status, read_capabilities, read_flow)
= parse_handshake(UntrustedRlp::new(&handshake)).unwrap(); = parse_handshake(UntrustedRlp::new(&handshake)).unwrap();
assert_eq!(read_status, status); assert_eq!(read_status, status);
assert_eq!(read_capabilities, capabilities); assert_eq!(read_capabilities, capabilities);
assert_eq!(read_flow, flow_params); assert_eq!(read_flow.unwrap(), flow_params);
} }
#[test] #[test]
fn partial_handshake() { fn partial_handshake() {
let status = Status { let status = Status {
protocol_version: 1, protocol_version: 1,
network_id: NetworkId::Mainnet, network_id: 1,
head_td: U256::default(), head_td: U256::default(),
head_hash: H256::default(), head_hash: H256::default(),
head_num: 10, head_num: 10,
@ -441,21 +426,21 @@ mod tests {
1000.into(), 1000.into(),
); );
let handshake = write_handshake(&status, &capabilities, &flow_params); let handshake = write_handshake(&status, &capabilities, Some(&flow_params));
let (read_status, read_capabilities, read_flow) let (read_status, read_capabilities, read_flow)
= parse_handshake(UntrustedRlp::new(&handshake)).unwrap(); = parse_handshake(UntrustedRlp::new(&handshake)).unwrap();
assert_eq!(read_status, status); assert_eq!(read_status, status);
assert_eq!(read_capabilities, capabilities); assert_eq!(read_capabilities, capabilities);
assert_eq!(read_flow, flow_params); assert_eq!(read_flow.unwrap(), flow_params);
} }
#[test] #[test]
fn skip_unknown_keys() { fn skip_unknown_keys() {
let status = Status { let status = Status {
protocol_version: 1, protocol_version: 1,
network_id: NetworkId::Mainnet, network_id: 1,
head_td: U256::default(), head_td: U256::default(),
head_hash: H256::default(), head_hash: H256::default(),
head_num: 10, head_num: 10,
@ -476,7 +461,7 @@ mod tests {
1000.into(), 1000.into(),
); );
let handshake = write_handshake(&status, &capabilities, &flow_params); let handshake = write_handshake(&status, &capabilities, Some(&flow_params));
let interleaved = { let interleaved = {
let handshake = UntrustedRlp::new(&handshake); let handshake = UntrustedRlp::new(&handshake);
let mut stream = RlpStream::new_list(handshake.item_count() * 3); let mut stream = RlpStream::new_list(handshake.item_count() * 3);
@ -498,7 +483,7 @@ mod tests {
assert_eq!(read_status, status); assert_eq!(read_status, status);
assert_eq!(read_capabilities, capabilities); assert_eq!(read_capabilities, capabilities);
assert_eq!(read_flow, flow_params); assert_eq!(read_flow.unwrap(), flow_params);
} }
#[test] #[test]
@ -548,4 +533,33 @@ mod tests {
let out = stream.drain(); let out = stream.drain();
assert!(parse_announcement(UntrustedRlp::new(&out)).is_ok()); assert!(parse_announcement(UntrustedRlp::new(&out)).is_ok());
} }
#[test]
fn optional_flow() {
let status = Status {
protocol_version: 1,
network_id: 1,
head_td: U256::default(),
head_hash: H256::default(),
head_num: 10,
genesis_hash: H256::zero(),
last_head: None,
};
let capabilities = Capabilities {
serve_headers: true,
serve_chain_since: Some(5),
serve_state_since: Some(8),
tx_relay: true,
};
let handshake = write_handshake(&status, &capabilities, None);
let (read_status, read_capabilities, read_flow)
= parse_handshake(UntrustedRlp::new(&handshake)).unwrap();
assert_eq!(read_status, status);
assert_eq!(read_capabilities, capabilities);
assert!(read_flow.is_none());
}
} }

View File

@ -0,0 +1,512 @@
// 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/>.
//! Tests for the `LightProtocol` implementation.
//! These don't test of the higher level logic on top of
use ethcore::blockchain_info::BlockChainInfo;
use ethcore::client::{BlockChainClient, EachBlockWith, TestBlockChainClient};
use ethcore::ids::BlockID;
use ethcore::transaction::SignedTransaction;
use network::PeerId;
use net::buffer_flow::FlowParams;
use net::context::IoContext;
use net::status::{Capabilities, Status, write_handshake};
use net::{encode_request, LightProtocol, Params, packet};
use provider::Provider;
use request::{self, Request, Headers};
use rlp::*;
use util::{Bytes, H256, U256};
use std::sync::Arc;
// expected result from a call.
#[derive(Debug, PartialEq, Eq)]
enum Expect {
/// Expect to have message sent to peer.
Send(PeerId, u8, Vec<u8>),
/// Expect this response.
Respond(u8, Vec<u8>),
/// Expect a punishment (disconnect/disable)
Punish(PeerId),
/// Expect nothing.
Nothing,
}
impl IoContext for Expect {
fn send(&self, peer: PeerId, packet_id: u8, packet_body: Vec<u8>) {
assert_eq!(self, &Expect::Send(peer, packet_id, packet_body));
}
fn respond(&self, packet_id: u8, packet_body: Vec<u8>) {
assert_eq!(self, &Expect::Respond(packet_id, packet_body));
}
fn disconnect_peer(&self, peer: PeerId) {
assert_eq!(self, &Expect::Punish(peer));
}
fn disable_peer(&self, peer: PeerId) {
assert_eq!(self, &Expect::Punish(peer));
}
fn protocol_version(&self, _peer: PeerId) -> Option<u8> {
Some(super::MAX_PROTOCOL_VERSION)
}
}
// can't implement directly for Arc due to cross-crate orphan rules.
struct TestProvider(Arc<TestProviderInner>);
struct TestProviderInner {
client: TestBlockChainClient,
}
impl Provider for TestProvider {
fn chain_info(&self) -> BlockChainInfo {
self.0.client.chain_info()
}
fn reorg_depth(&self, a: &H256, b: &H256) -> Option<u64> {
self.0.client.tree_route(a, b).map(|route| route.index as u64)
}
fn earliest_state(&self) -> Option<u64> {
None
}
fn block_headers(&self, req: request::Headers) -> Vec<Bytes> {
let best_num = self.0.client.chain_info().best_block_number;
let start_num = req.block_num;
match self.0.client.block_hash(BlockID::Number(req.block_num)) {
Some(hash) if hash == req.block_hash => {}
_=> {
trace!(target: "les_provider", "unknown/non-canonical start block in header request: {:?}", (req.block_num, req.block_hash));
return vec![]
}
}
(0u64..req.max as u64)
.map(|x: u64| x.saturating_mul(req.skip + 1))
.take_while(|x| if req.reverse { x < &start_num } else { best_num - start_num >= *x })
.map(|x| if req.reverse { start_num - x } else { start_num + x })
.map(|x| self.0.client.block_header(BlockID::Number(x)))
.take_while(|x| x.is_some())
.flat_map(|x| x)
.collect()
}
fn block_bodies(&self, req: request::Bodies) -> Vec<Bytes> {
req.block_hashes.into_iter()
.map(|hash| self.0.client.block_body(BlockID::Hash(hash)))
.map(|body| body.unwrap_or_else(|| ::rlp::EMPTY_LIST_RLP.to_vec()))
.collect()
}
fn receipts(&self, req: request::Receipts) -> Vec<Bytes> {
req.block_hashes.into_iter()
.map(|hash| self.0.client.block_receipts(&hash))
.map(|receipts| receipts.unwrap_or_else(|| ::rlp::EMPTY_LIST_RLP.to_vec()))
.collect()
}
fn proofs(&self, req: request::StateProofs) -> Vec<Bytes> {
req.requests.into_iter()
.map(|req| {
match req.key2 {
Some(_) => ::util::sha3::SHA3_NULL_RLP.to_vec(),
None => {
// sort of a leaf node
let mut stream = RlpStream::new_list(2);
stream.append(&req.key1).append_empty_data();
stream.out()
}
}
})
.collect()
}
fn contract_code(&self, req: request::ContractCodes) -> Vec<Bytes> {
req.code_requests.into_iter()
.map(|req| {
req.account_key.iter().chain(req.account_key.iter()).cloned().collect()
})
.collect()
}
fn header_proofs(&self, req: request::HeaderProofs) -> Vec<Bytes> {
req.requests.into_iter().map(|_| ::rlp::EMPTY_LIST_RLP.to_vec()).collect()
}
fn pending_transactions(&self) -> Vec<SignedTransaction> {
self.0.client.pending_transactions()
}
}
fn make_flow_params() -> FlowParams {
FlowParams::new(5_000_000.into(), Default::default(), 100_000.into())
}
fn capabilities() -> Capabilities {
Capabilities {
serve_headers: true,
serve_chain_since: Some(1),
serve_state_since: Some(1),
tx_relay: true,
}
}
// helper for setting up the protocol handler and provider.
fn setup(flow_params: FlowParams, capabilities: Capabilities) -> (Arc<TestProviderInner>, LightProtocol) {
let provider = Arc::new(TestProviderInner {
client: TestBlockChainClient::new(),
});
let proto = LightProtocol::new(Arc::new(TestProvider(provider.clone())), Params {
network_id: 2,
flow_params: flow_params,
capabilities: capabilities,
});
(provider, proto)
}
fn status(chain_info: BlockChainInfo) -> Status {
Status {
protocol_version: 1,
network_id: 2,
head_td: chain_info.total_difficulty,
head_hash: chain_info.best_block_hash,
head_num: chain_info.best_block_number,
genesis_hash: chain_info.genesis_hash,
last_head: None,
}
}
#[test]
fn handshake_expected() {
let flow_params = make_flow_params();
let capabilities = capabilities();
let (provider, proto) = setup(flow_params.clone(), capabilities.clone());
let status = status(provider.client.chain_info());
let packet_body = write_handshake(&status, &capabilities, Some(&flow_params));
proto.on_connect(&1, &Expect::Send(1, packet::STATUS, packet_body));
}
#[test]
#[should_panic]
fn genesis_mismatch() {
let flow_params = make_flow_params();
let capabilities = capabilities();
let (provider, proto) = setup(flow_params.clone(), capabilities.clone());
let mut status = status(provider.client.chain_info());
status.genesis_hash = H256::default();
let packet_body = write_handshake(&status, &capabilities, Some(&flow_params));
proto.on_connect(&1, &Expect::Send(1, packet::STATUS, packet_body));
}
#[test]
fn buffer_overflow() {
let flow_params = make_flow_params();
let capabilities = capabilities();
let (provider, proto) = setup(flow_params.clone(), capabilities.clone());
let status = status(provider.client.chain_info());
{
let packet_body = write_handshake(&status, &capabilities, Some(&flow_params));
proto.on_connect(&1, &Expect::Send(1, packet::STATUS, packet_body));
}
{
let my_status = write_handshake(&status, &capabilities, Some(&flow_params));
proto.handle_packet(&Expect::Nothing, &1, packet::STATUS, &my_status);
}
// 1000 requests is far too many for the default flow params.
let request = encode_request(&Request::Headers(Headers {
block_num: 1,
block_hash: provider.client.chain_info().genesis_hash,
max: 1000,
skip: 0,
reverse: false,
}), 111);
proto.handle_packet(&Expect::Punish(1), &1, packet::GET_BLOCK_HEADERS, &request);
}
// test the basic request types -- these just make sure that requests are parsed
// and sent to the provider correctly as well as testing response formatting.
#[test]
fn get_block_headers() {
let flow_params = FlowParams::new(5_000_000.into(), Default::default(), 0.into());
let capabilities = capabilities();
let (provider, proto) = setup(flow_params.clone(), capabilities.clone());
let cur_status = status(provider.client.chain_info());
let my_status = write_handshake(&cur_status, &capabilities, Some(&flow_params));
provider.client.add_blocks(100, EachBlockWith::Nothing);
let cur_status = status(provider.client.chain_info());
{
let packet_body = write_handshake(&cur_status, &capabilities, Some(&flow_params));
proto.on_connect(&1, &Expect::Send(1, packet::STATUS, packet_body));
proto.handle_packet(&Expect::Nothing, &1, packet::STATUS, &my_status);
}
let request = Headers {
block_num: 1,
block_hash: provider.client.block_hash(BlockID::Number(1)).unwrap(),
max: 10,
skip: 0,
reverse: false,
};
let req_id = 111;
let request_body = encode_request(&Request::Headers(request.clone()), req_id);
let response = {
let headers: Vec<_> = (0..10).map(|i| provider.client.block_header(BlockID::Number(i + 1)).unwrap()).collect();
assert_eq!(headers.len(), 10);
let new_buf = *flow_params.limit() - flow_params.compute_cost(request::Kind::Headers, 10);
let mut response_stream = RlpStream::new_list(12);
response_stream.append(&req_id).append(&new_buf);
for header in headers {
response_stream.append_raw(&header, 1);
}
response_stream.out()
};
let expected = Expect::Respond(packet::BLOCK_HEADERS, response);
proto.handle_packet(&expected, &1, packet::GET_BLOCK_HEADERS, &request_body);
}
#[test]
fn get_block_bodies() {
let flow_params = FlowParams::new(5_000_000.into(), Default::default(), 0.into());
let capabilities = capabilities();
let (provider, proto) = setup(flow_params.clone(), capabilities.clone());
let cur_status = status(provider.client.chain_info());
let my_status = write_handshake(&cur_status, &capabilities, Some(&flow_params));
provider.client.add_blocks(100, EachBlockWith::Nothing);
let cur_status = status(provider.client.chain_info());
{
let packet_body = write_handshake(&cur_status, &capabilities, Some(&flow_params));
proto.on_connect(&1, &Expect::Send(1, packet::STATUS, packet_body));
proto.handle_packet(&Expect::Nothing, &1, packet::STATUS, &my_status);
}
let request = request::Bodies {
block_hashes: (0..10).map(|i| provider.client.block_hash(BlockID::Number(i)).unwrap()).collect(),
};
let req_id = 111;
let request_body = encode_request(&Request::Bodies(request.clone()), req_id);
let response = {
let bodies: Vec<_> = (0..10).map(|i| provider.client.block_body(BlockID::Number(i + 1)).unwrap()).collect();
assert_eq!(bodies.len(), 10);
let new_buf = *flow_params.limit() - flow_params.compute_cost(request::Kind::Bodies, 10);
let mut response_stream = RlpStream::new_list(12);
response_stream.append(&req_id).append(&new_buf);
for body in bodies {
response_stream.append_raw(&body, 1);
}
response_stream.out()
};
let expected = Expect::Respond(packet::BLOCK_BODIES, response);
proto.handle_packet(&expected, &1, packet::GET_BLOCK_BODIES, &request_body);
}
#[test]
fn get_block_receipts() {
let flow_params = FlowParams::new(5_000_000.into(), Default::default(), 0.into());
let capabilities = capabilities();
let (provider, proto) = setup(flow_params.clone(), capabilities.clone());
let cur_status = status(provider.client.chain_info());
let my_status = write_handshake(&cur_status, &capabilities, Some(&flow_params));
provider.client.add_blocks(1000, EachBlockWith::Nothing);
let cur_status = status(provider.client.chain_info());
{
let packet_body = write_handshake(&cur_status, &capabilities, Some(&flow_params));
proto.on_connect(&1, &Expect::Send(1, packet::STATUS, packet_body));
proto.handle_packet(&Expect::Nothing, &1, packet::STATUS, &my_status);
}
// find the first 10 block hashes starting with `f` because receipts are only provided
// by the test client in that case.
let block_hashes: Vec<_> = (0..1000).map(|i| provider.client.block_hash(BlockID::Number(i)).unwrap())
.filter(|hash| format!("{}", hash).starts_with("f")).take(10).collect();
let request = request::Receipts {
block_hashes: block_hashes.clone(),
};
let req_id = 111;
let request_body = encode_request(&Request::Receipts(request.clone()), req_id);
let response = {
let receipts: Vec<_> = block_hashes.iter()
.map(|hash| provider.client.block_receipts(hash).unwrap())
.collect();
let new_buf = *flow_params.limit() - flow_params.compute_cost(request::Kind::Receipts, receipts.len());
let mut response_stream = RlpStream::new_list(2 + receipts.len());
response_stream.append(&req_id).append(&new_buf);
for block_receipts in receipts {
response_stream.append_raw(&block_receipts, 1);
}
response_stream.out()
};
let expected = Expect::Respond(packet::RECEIPTS, response);
proto.handle_packet(&expected, &1, packet::GET_RECEIPTS, &request_body);
}
#[test]
fn get_state_proofs() {
let flow_params = FlowParams::new(5_000_000.into(), Default::default(), 0.into());
let capabilities = capabilities();
let (provider, proto) = setup(flow_params.clone(), capabilities.clone());
let cur_status = status(provider.client.chain_info());
{
let packet_body = write_handshake(&cur_status, &capabilities, Some(&flow_params));
proto.on_connect(&1, &Expect::Send(1, packet::STATUS, packet_body.clone()));
proto.handle_packet(&Expect::Nothing, &1, packet::STATUS, &packet_body);
}
let req_id = 112;
let key1 = U256::from(11223344).into();
let key2 = U256::from(99988887).into();
let request = Request::StateProofs (request::StateProofs {
requests: vec![
request::StateProof { block: H256::default(), key1: key1, key2: None, from_level: 0 },
request::StateProof { block: H256::default(), key1: key1, key2: Some(key2), from_level: 0},
]
});
let request_body = encode_request(&request, req_id);
let response = {
let proofs = vec![
{ let mut stream = RlpStream::new_list(2); stream.append(&key1).append_empty_data(); stream.out() },
::util::sha3::SHA3_NULL_RLP.to_vec(),
];
let new_buf = *flow_params.limit() - flow_params.compute_cost(request::Kind::StateProofs, 2);
let mut response_stream = RlpStream::new_list(4);
response_stream.append(&req_id).append(&new_buf);
for proof in proofs {
response_stream.append_raw(&proof, 1);
}
response_stream.out()
};
let expected = Expect::Respond(packet::PROOFS, response);
proto.handle_packet(&expected, &1, packet::GET_PROOFS, &request_body);
}
#[test]
fn get_contract_code() {
let flow_params = FlowParams::new(5_000_000.into(), Default::default(), 0.into());
let capabilities = capabilities();
let (provider, proto) = setup(flow_params.clone(), capabilities.clone());
let cur_status = status(provider.client.chain_info());
{
let packet_body = write_handshake(&cur_status, &capabilities, Some(&flow_params));
proto.on_connect(&1, &Expect::Send(1, packet::STATUS, packet_body.clone()));
proto.handle_packet(&Expect::Nothing, &1, packet::STATUS, &packet_body);
}
let req_id = 112;
let key1 = U256::from(11223344).into();
let key2 = U256::from(99988887).into();
let request = Request::Codes (request::ContractCodes {
code_requests: vec![
request::ContractCode { block_hash: H256::default(), account_key: key1 },
request::ContractCode { block_hash: H256::default(), account_key: key2 },
],
});
let request_body = encode_request(&request, req_id);
let response = {
let codes: Vec<Vec<_>> = vec![
key1.iter().chain(key1.iter()).cloned().collect(),
key2.iter().chain(key2.iter()).cloned().collect(),
];
let new_buf = *flow_params.limit() - flow_params.compute_cost(request::Kind::Codes, 2);
let mut response_stream = RlpStream::new_list(4);
response_stream.append(&req_id).append(&new_buf);
for code in codes {
response_stream.append(&code);
}
response_stream.out()
};
let expected = Expect::Respond(packet::CONTRACT_CODES, response);
proto.handle_packet(&expected, &1, packet::GET_CONTRACT_CODES, &request_body);
}

View File

@ -33,6 +33,7 @@ use request;
/// or empty vector where appropriate. /// or empty vector where appropriate.
/// ///
/// [1]: https://github.com/ethcore/parity/wiki/Light-Ethereum-Subprotocol-(LES) /// [1]: https://github.com/ethcore/parity/wiki/Light-Ethereum-Subprotocol-(LES)
#[cfg_attr(feature = "ipc", ipc(client_ident="LightProviderClient"))]
pub trait Provider: Send + Sync { pub trait Provider: Send + Sync {
/// Provide current blockchain info. /// Provide current blockchain info.
fn chain_info(&self) -> BlockChainInfo; fn chain_info(&self) -> BlockChainInfo;
@ -71,7 +72,10 @@ pub trait Provider: Send + Sync {
/// Each item in the resulting vector is either the raw bytecode or empty. /// Each item in the resulting vector is either the raw bytecode or empty.
fn contract_code(&self, req: request::ContractCodes) -> Vec<Bytes>; fn contract_code(&self, req: request::ContractCodes) -> Vec<Bytes>;
/// Provide header proofs from the Canonical Hash Tries. /// Provide header proofs from the Canonical Hash Tries as well as the headers
/// they correspond to -- each element in the returned vector is a 2-tuple.
/// The first element is a block header and the second a merkle proof of
/// the header in a requested CHT.
fn header_proofs(&self, req: request::HeaderProofs) -> Vec<Bytes>; fn header_proofs(&self, req: request::HeaderProofs) -> Vec<Bytes>;
/// Provide pending transactions. /// Provide pending transactions.
@ -105,8 +109,8 @@ impl<T: ProvingBlockChainClient + ?Sized> Provider for T {
} }
(0u64..req.max as u64) (0u64..req.max as u64)
.map(|x: u64| x.saturating_mul(req.skip)) .map(|x: u64| x.saturating_mul(req.skip + 1))
.take_while(|x| if req.reverse { x < &start_num } else { best_num - start_num < *x }) .take_while(|x| if req.reverse { x < &start_num } else { best_num - start_num >= *x })
.map(|x| if req.reverse { start_num - x } else { start_num + x }) .map(|x| if req.reverse { start_num - x } else { start_num + x })
.map(|x| self.block_header(BlockID::Number(x))) .map(|x| self.block_header(BlockID::Number(x)))
.take_while(|x| x.is_some()) .take_while(|x| x.is_some())

View File

@ -19,7 +19,8 @@
use util::H256; use util::H256;
/// A request for block headers. /// A request for block headers.
#[derive(Debug, Clone, PartialEq, Eq, Binary)] #[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "ipc", derive(Binary))]
pub struct Headers { pub struct Headers {
/// Starting block number /// Starting block number
pub block_num: u64, pub block_num: u64,
@ -35,7 +36,8 @@ pub struct Headers {
} }
/// A request for specific block bodies. /// A request for specific block bodies.
#[derive(Debug, Clone, PartialEq, Eq, Binary)] #[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "ipc", derive(Binary))]
pub struct Bodies { pub struct Bodies {
/// Hashes which bodies are being requested for. /// Hashes which bodies are being requested for.
pub block_hashes: Vec<H256> pub block_hashes: Vec<H256>
@ -45,14 +47,16 @@ pub struct Bodies {
/// ///
/// This request is answered with a list of transaction receipts for each block /// This request is answered with a list of transaction receipts for each block
/// requested. /// requested.
#[derive(Debug, Clone, PartialEq, Eq, Binary)] #[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "ipc", derive(Binary))]
pub struct Receipts { pub struct Receipts {
/// Block hashes to return receipts for. /// Block hashes to return receipts for.
pub block_hashes: Vec<H256>, pub block_hashes: Vec<H256>,
} }
/// A request for a state proof /// A request for a state proof
#[derive(Debug, Clone, PartialEq, Eq, Binary)] #[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "ipc", derive(Binary))]
pub struct StateProof { pub struct StateProof {
/// Block hash to query state from. /// Block hash to query state from.
pub block: H256, pub block: H256,
@ -66,14 +70,16 @@ pub struct StateProof {
} }
/// A request for state proofs. /// A request for state proofs.
#[derive(Debug, Clone, PartialEq, Eq, Binary)] #[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "ipc", derive(Binary))]
pub struct StateProofs { pub struct StateProofs {
/// All the proof requests. /// All the proof requests.
pub requests: Vec<StateProof>, pub requests: Vec<StateProof>,
} }
/// A request for contract code. /// A request for contract code.
#[derive(Debug, Clone, PartialEq, Eq, Binary)] #[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "ipc", derive(Binary))]
pub struct ContractCode { pub struct ContractCode {
/// Block hash /// Block hash
pub block_hash: H256, pub block_hash: H256,
@ -82,14 +88,16 @@ pub struct ContractCode {
} }
/// A request for contract code. /// A request for contract code.
#[derive(Debug, Clone, PartialEq, Eq, Binary)] #[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "ipc", derive(Binary))]
pub struct ContractCodes { pub struct ContractCodes {
/// Block hash and account key (== sha3(address)) pairs to fetch code for. /// Block hash and account key (== sha3(address)) pairs to fetch code for.
pub code_requests: Vec<ContractCode>, pub code_requests: Vec<ContractCode>,
} }
/// A request for a header proof from the Canonical Hash Trie. /// A request for a header proof from the Canonical Hash Trie.
#[derive(Debug, Clone, PartialEq, Eq, Binary)] #[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "ipc", derive(Binary))]
pub struct HeaderProof { pub struct HeaderProof {
/// Number of the CHT. /// Number of the CHT.
pub cht_number: u64, pub cht_number: u64,
@ -100,14 +108,16 @@ pub struct HeaderProof {
} }
/// A request for header proofs from the CHT. /// A request for header proofs from the CHT.
#[derive(Debug, Clone, PartialEq, Eq, Binary)] #[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "ipc", derive(Binary))]
pub struct HeaderProofs { pub struct HeaderProofs {
/// All the proof requests. /// All the proof requests.
pub requests: Vec<HeaderProof>, pub requests: Vec<HeaderProof>,
} }
/// Kinds of requests. /// Kinds of requests.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Binary)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "ipc", derive(Binary))]
pub enum Kind { pub enum Kind {
/// Requesting headers. /// Requesting headers.
Headers, Headers,
@ -124,7 +134,8 @@ pub enum Kind {
} }
/// Encompasses all possible types of requests in a single structure. /// Encompasses all possible types of requests in a single structure.
#[derive(Debug, Clone, PartialEq, Eq, Binary)] #[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "ipc", derive(Binary))]
pub enum Request { pub enum Request {
/// Requesting headers. /// Requesting headers.
Headers(Headers), Headers(Headers),

View File

@ -15,6 +15,11 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! Types used in the public (IPC) api which require custom code generation. //! Types used in the public (IPC) api which require custom code generation.
#![cfg_attr(feature = "ipc", allow(dead_code, unused_assignments, unused_variables))] // codegen issues
#![allow(dead_code, unused_assignments, unused_variables)] // codegen issues
#[cfg(feature = "ipc")]
include!(concat!(env!("OUT_DIR"), "/mod.rs.in")); include!(concat!(env!("OUT_DIR"), "/mod.rs.in"));
#[cfg(not(feature = "ipc"))]
include!("mod.rs.in");

View File

@ -52,7 +52,7 @@ use blockchain::{BlockChain, BlockProvider, TreeRoute, ImportRoute};
use client::{ use client::{
BlockID, TransactionID, UncleID, TraceId, ClientConfig, BlockChainClient, BlockID, TransactionID, UncleID, TraceId, ClientConfig, BlockChainClient,
MiningBlockChainClient, TraceFilter, CallAnalytics, BlockImportError, Mode, MiningBlockChainClient, TraceFilter, CallAnalytics, BlockImportError, Mode,
ChainNotify, PruningInfo, ProvingBlockChainClient, ChainNotify, PruningInfo,
}; };
use client::Error as ClientError; use client::Error as ClientError;
use env_info::EnvInfo; use env_info::EnvInfo;
@ -580,13 +580,6 @@ impl Client {
self.miner.clone() self.miner.clone()
} }
/// Handle messages from the IO queue
pub fn handle_queued_message(&self, message: &Bytes) {
if let Err(e) = self.engine.handle_message(UntrustedRlp::new(message)) {
trace!(target: "poa", "Invalid message received: {}", e);
}
}
/// Used by PoA to try sealing on period change. /// Used by PoA to try sealing on period change.
pub fn update_sealing(&self) { pub fn update_sealing(&self) {
self.miner.update_sealing(self) self.miner.update_sealing(self)
@ -1096,6 +1089,10 @@ impl BlockChainClient for Client {
self.transaction_address(id).and_then(|address| self.chain.read().transaction(&address)) self.transaction_address(id).and_then(|address| self.chain.read().transaction(&address))
} }
fn transaction_block(&self, id: TransactionID) -> Option<H256> {
self.transaction_address(id).map(|addr| addr.block_hash)
}
fn uncle(&self, id: UncleID) -> Option<Bytes> { fn uncle(&self, id: UncleID) -> Option<Bytes> {
let index = id.position; let index = id.position;
self.block_body(id.block).and_then(|body| BodyView::new(&body).uncle_rlp_at(index)) self.block_body(id.block).and_then(|body| BodyView::new(&body).uncle_rlp_at(index))
@ -1434,7 +1431,7 @@ impl MayPanic for Client {
} }
} }
impl ProvingBlockChainClient for Client { impl ::client::ProvingBlockChainClient for Client {
fn prove_storage(&self, key1: H256, key2: H256, from_level: u32, id: BlockID) -> Vec<Bytes> { fn prove_storage(&self, key1: H256, key2: H256, from_level: u32, id: BlockID) -> Vec<Bytes> {
self.state_at(id) self.state_at(id)
.and_then(move |state| state.prove_storage(key1, key2, from_level).ok()) .and_then(move |state| state.prove_storage(key1, key2, from_level).ok())

View File

@ -27,7 +27,9 @@ pub use self::config::{Mode, ClientConfig, DatabaseCompactionProfile, BlockChain
pub use self::error::Error; pub use self::error::Error;
pub use self::test_client::{TestBlockChainClient, EachBlockWith}; pub use self::test_client::{TestBlockChainClient, EachBlockWith};
pub use self::chain_notify::ChainNotify; pub use self::chain_notify::ChainNotify;
pub use self::traits::{BlockChainClient, MiningBlockChainClient, ProvingBlockChainClient}; pub use self::traits::{BlockChainClient, MiningBlockChainClient};
pub use self::traits::ProvingBlockChainClient;
pub use types::ids::*; pub use types::ids::*;
pub use types::trace_filter::Filter as TraceFilter; pub use types::trace_filter::Filter as TraceFilter;

View File

@ -92,8 +92,8 @@ pub struct TestBlockChainClient {
pub first_block: RwLock<Option<(H256, u64)>>, pub first_block: RwLock<Option<(H256, u64)>>,
} }
#[derive(Clone)]
/// Used for generating test client blocks. /// Used for generating test client blocks.
#[derive(Clone)]
pub enum EachBlockWith { pub enum EachBlockWith {
/// Plain block. /// Plain block.
Nothing, Nothing,
@ -434,6 +434,10 @@ impl BlockChainClient for TestBlockChainClient {
None // Simple default. None // Simple default.
} }
fn transaction_block(&self, _id: TransactionID) -> Option<H256> {
None // Simple default.
}
fn uncle(&self, _id: UncleID) -> Option<Bytes> { fn uncle(&self, _id: UncleID) -> Option<Bytes> {
None // Simple default. None // Simple default.
} }

View File

@ -129,6 +129,9 @@ pub trait BlockChainClient : Sync + Send {
/// Get transaction with given hash. /// Get transaction with given hash.
fn transaction(&self, id: TransactionID) -> Option<LocalizedTransaction>; fn transaction(&self, id: TransactionID) -> Option<LocalizedTransaction>;
/// Get the hash of block that contains the transaction, if any.
fn transaction_block(&self, id: TransactionID) -> Option<H256>;
/// Get uncle with given id. /// Get uncle with given id.
fn uncle(&self, id: UncleID) -> Option<Bytes>; fn uncle(&self, id: UncleID) -> Option<Bytes>;

View File

@ -23,7 +23,7 @@ use account_provider::{AccountProvider, Error as AccountError};
use views::{BlockView, HeaderView}; use views::{BlockView, HeaderView};
use header::Header; use header::Header;
use state::{State, CleanupMode}; use state::{State, CleanupMode};
use client::{MiningBlockChainClient, Executive, Executed, EnvInfo, TransactOptions, BlockID, CallAnalytics}; use client::{MiningBlockChainClient, Executive, Executed, EnvInfo, TransactOptions, BlockID, CallAnalytics, TransactionID};
use client::TransactionImportResult; use client::TransactionImportResult;
use executive::contract_address; use executive::contract_address;
use block::{ClosedBlock, IsBlock, Block}; use block::{ClosedBlock, IsBlock, Block};
@ -357,6 +357,8 @@ impl Miner {
let block_number = open_block.block().fields().header.number(); let block_number = open_block.block().fields().header.number();
// TODO Push new uncles too. // TODO Push new uncles too.
let mut tx_count: usize = 0;
let tx_total = transactions.len();
for tx in transactions { for tx in transactions {
let hash = tx.hash(); let hash = tx.hash();
let start = Instant::now(); let start = Instant::now();
@ -378,7 +380,7 @@ impl Miner {
}, },
_ => {}, _ => {},
} }
trace!(target: "miner", "Adding tx {:?} took {:?}", hash, took);
match result { match result {
Err(Error::Execution(ExecutionError::BlockGasLimitReached { gas_limit, gas_used, gas })) => { Err(Error::Execution(ExecutionError::BlockGasLimitReached { gas_limit, gas_used, gas })) => {
debug!(target: "miner", "Skipping adding transaction to block because of gas limit: {:?} (limit: {:?}, used: {:?}, gas: {:?})", hash, gas_limit, gas_used, gas); debug!(target: "miner", "Skipping adding transaction to block because of gas limit: {:?} (limit: {:?}, used: {:?}, gas: {:?})", hash, gas_limit, gas_used, gas);
@ -407,9 +409,12 @@ impl Miner {
"Error adding transaction to block: number={}. transaction_hash={:?}, Error: {:?}", "Error adding transaction to block: number={}. transaction_hash={:?}, Error: {:?}",
block_number, hash, e); block_number, hash, e);
}, },
_ => {} // imported ok _ => {
tx_count += 1;
} // imported ok
} }
} }
trace!(target: "miner", "Pushed {}/{} transactions", tx_count, tx_total);
let block = open_block.close(); let block = open_block.close();
@ -589,6 +594,10 @@ impl Miner {
let best_block_header: Header = ::rlp::decode(&chain.best_block_header()); let best_block_header: Header = ::rlp::decode(&chain.best_block_header());
transactions.into_iter() transactions.into_iter()
.map(|tx| { .map(|tx| {
if chain.transaction_block(TransactionID::Hash(tx.hash())).is_some() {
debug!(target: "miner", "Rejected tx {:?}: already in the blockchain", tx.hash());
return Err(Error::Transaction(TransactionError::AlreadyImported));
}
match self.engine.verify_transaction_basic(&tx, &best_block_header) { match self.engine.verify_transaction_basic(&tx, &best_block_header) {
Err(e) => { Err(e) => {
debug!(target: "miner", "Rejected tx {:?} with invalid signature: {:?}", tx.hash(), e); debug!(target: "miner", "Rejected tx {:?} with invalid signature: {:?}", tx.hash(), e);

View File

@ -17,6 +17,7 @@
//! Creates and registers client and network services. //! Creates and registers client and network services.
use util::*; use util::*;
use rlp::{UntrustedRlp, View};
use io::*; use io::*;
use spec::Spec; use spec::Spec;
use error::*; use error::*;
@ -227,7 +228,9 @@ impl IoHandler<ClientIoMessage> for ClientIoHandler {
ClientIoMessage::UpdateSealing => self.client.update_sealing(), ClientIoMessage::UpdateSealing => self.client.update_sealing(),
ClientIoMessage::SubmitSeal(ref hash, ref seal) => self.client.submit_seal(*hash, seal.clone()), ClientIoMessage::SubmitSeal(ref hash, ref seal) => self.client.submit_seal(*hash, seal.clone()),
ClientIoMessage::BroadcastMessage(ref message) => self.client.broadcast_message(message.clone()), ClientIoMessage::BroadcastMessage(ref message) => self.client.broadcast_message(message.clone()),
ClientIoMessage::NewMessage(ref message) => self.client.handle_queued_message(message), ClientIoMessage::NewMessage(ref message) => if let Err(e) = self.client.engine().handle_message(UntrustedRlp::new(message)) {
trace!(target: "poa", "Invalid message received: {}", e);
},
_ => {} // ignore other messages _ => {} // ignore other messages
} }
} }

View File

@ -31,6 +31,7 @@ use transaction::SignedTransaction;
use state_db::StateDB; use state_db::StateDB;
use util::*; use util::*;
use util::trie::recorder::{Recorder, BasicRecorder as TrieRecorder}; use util::trie::recorder::{Recorder, BasicRecorder as TrieRecorder};
mod account; mod account;

View File

@ -7,6 +7,7 @@
"transform-runtime", "transform-runtime",
"transform-decorators-legacy", "transform-decorators-legacy",
"transform-class-properties", "transform-class-properties",
"transform-object-rest-spread",
"lodash" "lodash"
], ],
"retainLines": true, "retainLines": true,
@ -16,6 +17,15 @@
}, },
"development": { "development": {
"plugins": ["react-hot-loader/babel"] "plugins": ["react-hot-loader/babel"]
},
"test": {
"plugins": [
[
"babel-plugin-webpack-alias", {
"config": "webpack/test.js"
}
]
]
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "parity.js", "name": "parity.js",
"version": "0.2.99", "version": "0.2.105",
"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>",
@ -40,33 +40,36 @@
"coveralls": "npm run testCoverage && coveralls < coverage/lcov.info", "coveralls": "npm run testCoverage && coveralls < coverage/lcov.info",
"lint": "eslint --ignore-path .gitignore ./src/", "lint": "eslint --ignore-path .gitignore ./src/",
"lint:cached": "eslint --cache --ignore-path .gitignore ./src/", "lint:cached": "eslint --cache --ignore-path .gitignore ./src/",
"test": "mocha 'src/**/*.spec.js'", "test": "NODE_ENV=test mocha 'src/**/*.spec.js'",
"test:coverage": "istanbul cover _mocha -- 'src/**/*.spec.js'", "test:coverage": "NODE_ENV=test istanbul cover _mocha -- 'src/**/*.spec.js'",
"test:e2e": "mocha 'src/**/*.e2e.js'", "test:e2e": "NODE_ENV=test mocha 'src/**/*.e2e.js'",
"test:npm": "(cd .npmjs && npm i) && node test/npmLibrary && (rm -rf .npmjs/node_modules)", "test:npm": "(cd .npmjs && npm i) && node test/npmLibrary && (rm -rf .npmjs/node_modules)",
"prepush": "npm run lint:cached" "prepush": "npm run lint:cached"
}, },
"devDependencies": { "devDependencies": {
"babel-cli": "6.18.0", "babel-cli": "6.18.0",
"babel-core": "6.18.2", "babel-core": "6.20.0",
"babel-eslint": "7.1.1", "babel-eslint": "7.1.1",
"babel-loader": "6.2.8", "babel-loader": "6.2.8",
"babel-plugin-lodash": "3.2.10", "babel-plugin-lodash": "3.2.10",
"babel-plugin-transform-class-properties": "6.19.0", "babel-plugin-transform-class-properties": "6.18.0",
"babel-plugin-transform-decorators-legacy": "1.3.4", "babel-plugin-transform-decorators-legacy": "1.3.4",
"babel-plugin-transform-object-rest-spread": "6.20.2",
"babel-plugin-transform-react-remove-prop-types": "0.2.11", "babel-plugin-transform-react-remove-prop-types": "0.2.11",
"babel-plugin-transform-runtime": "6.15.0", "babel-plugin-transform-runtime": "6.15.0",
"babel-polyfill": "6.16.0", "babel-plugin-webpack-alias": "2.1.2",
"babel-polyfill": "6.20.0",
"babel-preset-es2015": "6.18.0", "babel-preset-es2015": "6.18.0",
"babel-preset-es2015-rollup": "1.2.0",
"babel-preset-es2016": "6.16.0", "babel-preset-es2016": "6.16.0",
"babel-preset-es2017": "6.16.0", "babel-preset-es2017": "6.16.0",
"babel-preset-react": "6.16.0", "babel-preset-react": "6.16.0",
"babel-preset-stage-0": "6.16.0", "babel-preset-stage-0": "6.16.0",
"babel-register": "6.18.0", "babel-register": "6.18.0",
"babel-runtime": "6.18.0", "babel-runtime": "6.20.0",
"chai": "3.5.0", "chai": "3.5.0",
"chai-as-promised": "6.0.0",
"chai-enzyme": "0.6.1", "chai-enzyme": "0.6.1",
"circular-dependency-plugin": "2.0.0",
"copy-webpack-plugin": "4.0.1", "copy-webpack-plugin": "4.0.1",
"core-js": "2.4.1", "core-js": "2.4.1",
"coveralls": "2.11.15", "coveralls": "2.11.15",
@ -98,8 +101,8 @@
"mock-local-storage": "1.0.2", "mock-local-storage": "1.0.2",
"mock-socket": "6.0.3", "mock-socket": "6.0.3",
"nock": "9.0.2", "nock": "9.0.2",
"postcss-import": "8.1.0", "postcss-import": "9.0.0",
"postcss-loader": "1.1.1", "postcss-loader": "1.2.0",
"postcss-nested": "1.0.0", "postcss-nested": "1.0.0",
"postcss-simple-vars": "3.0.0", "postcss-simple-vars": "3.0.0",
"progress": "1.1.8", "progress": "1.1.8",
@ -136,7 +139,7 @@
"js-sha3": "0.5.5", "js-sha3": "0.5.5",
"lodash": "4.17.2", "lodash": "4.17.2",
"marked": "0.3.6", "marked": "0.3.6",
"material-ui": "0.16.4", "material-ui": "0.16.5",
"material-ui-chip-input": "0.11.1", "material-ui-chip-input": "0.11.1",
"mobx": "2.6.4", "mobx": "2.6.4",
"mobx-react": "4.0.3", "mobx-react": "4.0.3",

View File

@ -1 +1 @@
// test script 4 // test script 6

View File

@ -14,10 +14,14 @@
// 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 const url = (isTestnet = false) => {
return `https://${isTestnet ? 'testnet.' : ''}etherscan.io`;
};
export const txLink = (hash, isTestnet = false) => { export const txLink = (hash, isTestnet = false) => {
return `https://${isTestnet ? 'testnet.' : ''}etherscan.io/tx/${hash}`; return `${url(isTestnet)}/tx/${hash}`;
}; };
export const addressLink = (address, isTestnet = false) => { export const addressLink = (address, isTestnet = false) => {
return `https://${isTestnet ? 'testnet.' : ''}etherscan.io/address/${address}`; return `${url(isTestnet)}/address/${address}`;
}; };

View File

@ -240,8 +240,8 @@ export default class Contract {
} }
_bindEvent = (event) => { _bindEvent = (event) => {
event.subscribe = (options = {}, callback) => { event.subscribe = (options = {}, callback, autoRemove) => {
return this._subscribe(event, options, callback); return this._subscribe(event, options, callback, autoRemove);
}; };
event.unsubscribe = (subscriptionId) => { event.unsubscribe = (subscriptionId) => {
@ -262,12 +262,11 @@ export default class Contract {
} }
const options = this._getFilterOptions(event, _options); const options = this._getFilterOptions(event, _options);
options.fromBlock = 0;
options.toBlock = 'latest';
return this._api.eth return this._api.eth
.getLogs({ .getLogs(options)
fromBlock: 0,
toBlock: 'latest',
...options
})
.then((logs) => this.parseEventLogs(logs)); .then((logs) => this.parseEventLogs(logs));
} }
@ -307,16 +306,31 @@ export default class Contract {
return this._api.eth.newFilter(options); return this._api.eth.newFilter(options);
} }
subscribe (eventName = null, options = {}, callback) { subscribe (eventName = null, options = {}, callback, autoRemove) {
try { try {
const event = this._findEvent(eventName); const event = this._findEvent(eventName);
return this._subscribe(event, options, callback); return this._subscribe(event, options, callback, autoRemove);
} catch (e) { } catch (e) {
return Promise.reject(e); return Promise.reject(e);
} }
} }
_subscribe (event = null, _options, callback) { _sendData (subscriptionId, error, logs) {
const { autoRemove, callback } = this._subscriptions[subscriptionId];
let result = true;
try {
result = callback(error, logs);
} catch (error) {
console.warn('_sendData', subscriptionId, error);
}
if (autoRemove && result && typeof result === 'boolean') {
this.unsubscribe(subscriptionId);
}
}
_subscribe (event = null, _options, callback, autoRemove = false) {
const subscriptionId = nextSubscriptionId++; const subscriptionId = nextSubscriptionId++;
const { skipInitFetch } = _options; const { skipInitFetch } = _options;
delete _options['skipInitFetch']; delete _options['skipInitFetch'];
@ -326,6 +340,7 @@ export default class Contract {
.then((filterId) => { .then((filterId) => {
this._subscriptions[subscriptionId] = { this._subscriptions[subscriptionId] = {
options: _options, options: _options,
autoRemove,
callback, callback,
filterId filterId
}; };
@ -338,8 +353,7 @@ export default class Contract {
return this._api.eth return this._api.eth
.getFilterLogs(filterId) .getFilterLogs(filterId)
.then((logs) => { .then((logs) => {
callback(null, this.parseEventLogs(logs)); this._sendData(subscriptionId, null, this.parseEventLogs(logs));
this._subscribeToChanges(); this._subscribeToChanges();
return subscriptionId; return subscriptionId;
}); });
@ -438,13 +452,13 @@ export default class Contract {
}) })
) )
.then((logsArray) => { .then((logsArray) => {
logsArray.forEach((logs, idx) => { logsArray.forEach((logs, subscriptionId) => {
if (!logs || !logs.length) { if (!logs || !logs.length) {
return; return;
} }
try { try {
subscriptions[idx].callback(null, this.parseEventLogs(logs)); this.sendData(subscriptionId, null, this.parseEventLogs(logs));
} catch (error) { } catch (error) {
console.error('_sendSubscriptionChanges', error); console.error('_sendSubscriptionChanges', error);
} }

View File

@ -59,7 +59,7 @@ export default class Manager {
return subscription; return subscription;
} }
subscribe (subscriptionName, callback) { subscribe (subscriptionName, callback, autoRemove = false) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const subscription = this._validateType(subscriptionName); const subscription = this._validateType(subscriptionName);
@ -75,6 +75,7 @@ export default class Manager {
this.subscriptions[subscriptionId] = { this.subscriptions[subscriptionId] = {
name: subscriptionName, name: subscriptionName,
id: subscriptionId, id: subscriptionId,
autoRemove,
callback callback
}; };
@ -101,13 +102,18 @@ export default class Manager {
} }
_sendData (subscriptionId, error, data) { _sendData (subscriptionId, error, data) {
const { callback } = this.subscriptions[subscriptionId]; const { autoRemove, callback } = this.subscriptions[subscriptionId];
let result = true;
try { try {
callback(error, data); result = callback(error, data);
} catch (error) { } catch (error) {
console.error(`Unable to update callback for subscriptionId ${subscriptionId}`, error); console.error(`Unable to update callback for subscriptionId ${subscriptionId}`, error);
} }
if (autoRemove && result && typeof result === 'boolean') {
this.unsubscribe(subscriptionId);
}
} }
_updateSubscriptions = (subscriptionName, error, data) => { _updateSubscriptions = (subscriptionName, error, data) => {

View File

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

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,10 @@ import * as abis from './abi';
export default class Registry { export default class Registry {
constructor (api) { constructor (api) {
this._api = api; this._api = api;
this._contracts = [];
this._contracts = {};
this._pendingContracts = {};
this._instance = null; this._instance = null;
this._fetching = false; this._fetching = false;
this._queue = []; this._queue = [];
@ -59,20 +62,25 @@ export default class Registry {
getContract (_name) { getContract (_name) {
const name = _name.toLowerCase(); const name = _name.toLowerCase();
return new Promise((resolve, reject) => {
if (this._contracts[name]) { if (this._contracts[name]) {
resolve(this._contracts[name]); return Promise.resolve(this._contracts[name]);
return;
} }
this if (this._pendingContracts[name]) {
return this._pendingContracts[name];
}
const promise = this
.lookupAddress(name) .lookupAddress(name)
.then((address) => { .then((address) => {
this._contracts[name] = this._api.newContract(abis[name], address); this._contracts[name] = this._api.newContract(abis[name], address);
resolve(this._contracts[name]); delete this._pendingContracts[name];
}) return this._contracts[name];
.catch(reject);
}); });
this._pendingContracts[name] = promise;
return promise;
} }
getContractInstance (_name) { getContractInstance (_name) {
@ -89,7 +97,7 @@ export default class Registry {
return instance.getAddress.call({}, [sha3, 'A']); return instance.getAddress.call({}, [sha3, 'A']);
}) })
.then((address) => { .then((address) => {
console.log('lookupAddress', name, sha3, address); console.log('[lookupAddress]', `(${sha3}) ${name}: ${address}`);
return address; return address;
}); });
} }

View File

@ -0,0 +1,460 @@
//sol Wallet
// Multi-sig, daily-limited account proxy/wallet.
// @authors:
// Gav Wood <g@ethdev.com>
// inheritable "property" contract that enables methods to be protected by requiring the acquiescence of either a
// single, or, crucially, each of a number of, designated owners.
// usage:
// use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by
// some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the
// interior is executed.
pragma solidity ^0.4.6;
contract multisig {
// EVENTS
// this contract can accept a confirmation, in which case
// we record owner and operation (hash) alongside it.
event Confirmation(address owner, bytes32 operation);
event Revoke(address owner, bytes32 operation);
// some others are in the case of an owner changing.
event OwnerChanged(address oldOwner, address newOwner);
event OwnerAdded(address newOwner);
event OwnerRemoved(address oldOwner);
// the last one is emitted if the required signatures change
event RequirementChanged(uint newRequirement);
// Funds has arrived into the wallet (record how much).
event Deposit(address _from, uint value);
// Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going).
event SingleTransact(address owner, uint value, address to, bytes data);
// Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going).
event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data);
// Confirmation still needed for a transaction.
event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data);
}
contract multisigAbi is multisig {
function isOwner(address _addr) returns (bool);
function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool);
function confirm(bytes32 _h) returns(bool);
// (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today.
function setDailyLimit(uint _newLimit);
function addOwner(address _owner);
function removeOwner(address _owner);
function changeRequirement(uint _newRequired);
// Revokes a prior confirmation of the given operation
function revoke(bytes32 _operation);
function changeOwner(address _from, address _to);
function execute(address _to, uint _value, bytes _data) returns(bool);
}
contract WalletLibrary is multisig {
// TYPES
// struct for the status of a pending operation.
struct PendingState {
uint yetNeeded;
uint ownersDone;
uint index;
}
// Transaction structure to remember details of transaction lest it need be saved for a later call.
struct Transaction {
address to;
uint value;
bytes data;
}
/******************************
***** MULTI OWNED SECTION ****
******************************/
// MODIFIERS
// simple single-sig function modifier.
modifier onlyowner {
if (isOwner(msg.sender))
_;
}
// multi-sig function modifier: the operation must have an intrinsic hash in order
// that later attempts can be realised as the same underlying operation and
// thus count as confirmations.
modifier onlymanyowners(bytes32 _operation) {
if (confirmAndCheck(_operation))
_;
}
// METHODS
// constructor is given number of sigs required to do protected "onlymanyowners" transactions
// as well as the selection of addresses capable of confirming them.
function initMultiowned(address[] _owners, uint _required) {
m_numOwners = _owners.length + 1;
m_owners[1] = uint(msg.sender);
m_ownerIndex[uint(msg.sender)] = 1;
m_required = _required;
for (uint i = 0; i < _owners.length; ++i)
{
m_owners[2 + i] = uint(_owners[i]);
m_ownerIndex[uint(_owners[i])] = 2 + i;
}
}
// Revokes a prior confirmation of the given operation
function revoke(bytes32 _operation) {
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return;
uint ownerIndexBit = 2**ownerIndex;
var pending = m_pending[_operation];
if (pending.ownersDone & ownerIndexBit > 0) {
pending.yetNeeded++;
pending.ownersDone -= ownerIndexBit;
Revoke(msg.sender, _operation);
}
}
// Replaces an owner `_from` with another `_to`.
function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) {
if (isOwner(_to)) return;
uint ownerIndex = m_ownerIndex[uint(_from)];
if (ownerIndex == 0) return;
clearPending();
m_owners[ownerIndex] = uint(_to);
m_ownerIndex[uint(_from)] = 0;
m_ownerIndex[uint(_to)] = ownerIndex;
OwnerChanged(_from, _to);
}
function addOwner(address _owner) onlymanyowners(sha3(msg.data)) {
if (isOwner(_owner)) return;
clearPending();
if (m_numOwners >= c_maxOwners)
reorganizeOwners();
if (m_numOwners >= c_maxOwners)
return;
m_numOwners++;
m_owners[m_numOwners] = uint(_owner);
m_ownerIndex[uint(_owner)] = m_numOwners;
OwnerAdded(_owner);
}
function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) {
uint ownerIndex = m_ownerIndex[uint(_owner)];
if (ownerIndex == 0) return;
if (m_required > m_numOwners - 1) return;
m_owners[ownerIndex] = 0;
m_ownerIndex[uint(_owner)] = 0;
clearPending();
reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot
OwnerRemoved(_owner);
}
function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) {
if (_newRequired > m_numOwners) return;
m_required = _newRequired;
clearPending();
RequirementChanged(_newRequired);
}
function isOwner(address _addr) returns (bool) {
return m_ownerIndex[uint(_addr)] > 0;
}
function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) {
var pending = m_pending[_operation];
uint ownerIndex = m_ownerIndex[uint(_owner)];
// make sure they're an owner
if (ownerIndex == 0) return false;
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
return !(pending.ownersDone & ownerIndexBit == 0);
}
// INTERNAL METHODS
function confirmAndCheck(bytes32 _operation) internal returns (bool) {
// determine what index the present sender is:
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return;
var pending = m_pending[_operation];
// if we're not yet working on this operation, switch over and reset the confirmation status.
if (pending.yetNeeded == 0) {
// reset count of confirmations needed.
pending.yetNeeded = m_required;
// reset which owners have confirmed (none) - set our bitmap to 0.
pending.ownersDone = 0;
pending.index = m_pendingIndex.length++;
m_pendingIndex[pending.index] = _operation;
}
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
// make sure we (the message sender) haven't confirmed this operation previously.
if (pending.ownersDone & ownerIndexBit == 0) {
Confirmation(msg.sender, _operation);
// ok - check if count is enough to go ahead.
if (pending.yetNeeded <= 1) {
// enough confirmations: reset and run interior.
delete m_pendingIndex[m_pending[_operation].index];
delete m_pending[_operation];
return true;
}
else
{
// not enough: record that this owner in particular confirmed.
pending.yetNeeded--;
pending.ownersDone |= ownerIndexBit;
}
}
}
function reorganizeOwners() private {
uint free = 1;
while (free < m_numOwners)
{
while (free < m_numOwners && m_owners[free] != 0) free++;
while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--;
if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0)
{
m_owners[free] = m_owners[m_numOwners];
m_ownerIndex[m_owners[free]] = free;
m_owners[m_numOwners] = 0;
}
}
}
function clearPending() internal {
uint length = m_pendingIndex.length;
for (uint i = 0; i < length; ++i)
if (m_pendingIndex[i] != 0)
delete m_pending[m_pendingIndex[i]];
delete m_pendingIndex;
}
/******************************
****** DAY LIMIT SECTION *****
******************************/
// MODIFIERS
// simple modifier for daily limit.
modifier limitedDaily(uint _value) {
if (underLimit(_value))
_;
}
// METHODS
// constructor - stores initial daily limit and records the present day's index.
function initDaylimit(uint _limit) {
m_dailyLimit = _limit;
m_lastDay = today();
}
// (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today.
function setDailyLimit(uint _newLimit) onlymanyowners(sha3(msg.data)) {
m_dailyLimit = _newLimit;
}
// resets the amount already spent today. needs many of the owners to confirm.
function resetSpentToday() onlymanyowners(sha3(msg.data)) {
m_spentToday = 0;
}
// INTERNAL METHODS
// checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and
// returns true. otherwise just returns false.
function underLimit(uint _value) internal onlyowner returns (bool) {
// reset the spend limit if we're on a different day to last time.
if (today() > m_lastDay) {
m_spentToday = 0;
m_lastDay = today();
}
// check to see if there's enough left - if so, subtract and return true.
// overflow protection // dailyLimit check
if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) {
m_spentToday += _value;
return true;
}
return false;
}
// determines today's index.
function today() private constant returns (uint) { return now / 1 days; }
/******************************
********* WALLET SECTION *****
******************************/
// METHODS
// constructor - just pass on the owner array to the multiowned and
// the limit to daylimit
function initWallet(address[] _owners, uint _required, uint _daylimit) {
initMultiowned(_owners, _required);
initDaylimit(_daylimit) ;
}
// kills the contract sending everything to `_to`.
function kill(address _to) onlymanyowners(sha3(msg.data)) {
suicide(_to);
}
// Outside-visible transact entry point. Executes transaction immediately if below daily spend limit.
// If not, goes into multisig process. We provide a hash on return to allow the sender to provide
// shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value
// and _data arguments). They still get the option of using them if they want, anyways.
function execute(address _to, uint _value, bytes _data) onlyowner returns(bool _callValue) {
// first, take the opportunity to check that we're under the daily limit.
if (underLimit(_value)) {
SingleTransact(msg.sender, _value, _to, _data);
// yes - just execute the call.
_callValue =_to.call.value(_value)(_data);
} else {
// determine our operation hash.
bytes32 _r = sha3(msg.data, block.number);
if (!confirm(_r) && m_txs[_r].to == 0) {
m_txs[_r].to = _to;
m_txs[_r].value = _value;
m_txs[_r].data = _data;
ConfirmationNeeded(_r, msg.sender, _value, _to, _data);
}
}
}
// confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order
// to determine the body of the transaction from the hash provided.
function confirm(bytes32 _h) onlymanyowners(_h) returns (bool) {
if (m_txs[_h].to != 0) {
m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data);
MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data);
delete m_txs[_h];
return true;
}
}
// INTERNAL METHODS
function clearWalletPending() internal {
uint length = m_pendingIndex.length;
for (uint i = 0; i < length; ++i)
delete m_txs[m_pendingIndex[i]];
clearPending();
}
// FIELDS
address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe;
// the number of owners that must confirm the same operation before it is run.
uint m_required;
// pointer used to find a free slot in m_owners
uint m_numOwners;
uint public m_dailyLimit;
uint public m_spentToday;
uint public m_lastDay;
// list of owners
uint[256] m_owners;
uint constant c_maxOwners = 250;
// index on the list of owners to allow reverse lookup
mapping(uint => uint) m_ownerIndex;
// the ongoing operations.
mapping(bytes32 => PendingState) m_pending;
bytes32[] m_pendingIndex;
// pending transactions we have at present.
mapping (bytes32 => Transaction) m_txs;
}
contract Wallet is multisig {
// WALLET CONSTRUCTOR
// calls the `initWallet` method of the Library in this context
function Wallet(address[] _owners, uint _required, uint _daylimit) {
// Signature of the Wallet Library's init function
bytes4 sig = bytes4(sha3("initWallet(address[],uint256,uint256)"));
address target = _walletLibrary;
// Compute the size of the call data : arrays has 2
// 32bytes for offset and length, plus 32bytes per element ;
// plus 2 32bytes for each uint
uint argarraysize = (2 + _owners.length);
uint argsize = (2 + argarraysize) * 32;
assembly {
// Add the signature first to memory
mstore(0x0, sig)
// Add the call data, which is at the end of the
// code
codecopy(0x4, sub(codesize, argsize), argsize)
// Delegate call to the library
delegatecall(sub(gas, 10000), target, 0x0, add(argsize, 0x4), 0x0, 0x0)
}
}
// METHODS
// gets called when no other function matches
function() payable {
// just being sent some cash?
if (msg.value > 0)
Deposit(msg.sender, msg.value);
else if (msg.data.length > 0)
_walletLibrary.delegatecall(msg.data);
}
// Gets an owner by 0-indexed position (using numOwners as the count)
function getOwner(uint ownerIndex) constant returns (address) {
return address(m_owners[ownerIndex + 1]);
}
// As return statement unavailable in fallback, explicit the method here
function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) {
return _walletLibrary.delegatecall(msg.data);
}
function isOwner(address _addr) returns (bool) {
return _walletLibrary.delegatecall(msg.data);
}
// FIELDS
address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe;
// the number of owners that must confirm the same operation before it is run.
uint public m_required;
// pointer used to find a free slot in m_owners
uint public m_numOwners;
uint public m_dailyLimit;
uint public m_spentToday;
uint public m_lastDay;
// list of owners
uint[256] m_owners;
}

View File

@ -21,7 +21,7 @@ import '../../../environment/tests';
import Application from './application'; import Application from './application';
describe('localtx/Application', () => { describe('dapps/localtx/Application', () => {
describe('rendering', () => { describe('rendering', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
const rendered = shallow(<Application />); const rendered = shallow(<Application />);

View File

@ -29,7 +29,7 @@ Api.api = {
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { Transaction, LocalTransaction } from './transaction'; import { Transaction, LocalTransaction } from './transaction';
describe('localtx/Transaction', () => { describe('dapps/localtx/Transaction', () => {
describe('rendering', () => { describe('rendering', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
const transaction = { const transaction = {
@ -51,7 +51,7 @@ describe('localtx/Transaction', () => {
}); });
}); });
describe('localtx/LocalTransaction', () => { describe('dapps/localtx/LocalTransaction', () => {
describe('rendering', () => { describe('rendering', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
const rendered = shallow( const rendered = shallow(

View File

@ -1,12 +0,0 @@
import babel from 'rollup-plugin-babel';
export default {
entry: 'src/index.js',
dest: 'release/index.js',
format: 'cjs',
plugins: [babel({
babelrc: false,
presets: ['es2015-rollup', 'stage-0'],
runtimeHelpers: true
})]
};

View File

@ -19,7 +19,7 @@ import ContentAdd from 'material-ui/svg-icons/content/add';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import { Button, Modal, Form, Input, InputAddress } from '~/ui'; import { Button, Modal, Form, Input, InputAddress } from '~/ui';
import { ERRORS, validateAddress, validateName } from '../../util/validation'; import { ERRORS, validateAddress, validateName } from '~/util/validation';
export default class AddAddress extends Component { export default class AddAddress extends Component {
static contextTypes = { static contextTypes = {

View File

@ -21,7 +21,7 @@ import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forwa
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back'; import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '~/ui'; import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '~/ui';
import { ERRORS, validateAbi, validateAddress, validateName } from '../../util/validation'; import { ERRORS, validateAbi, validateAddress, validateName } from '~/util/validation';
import { eip20, wallet } from '~/contracts/abi'; import { eip20, wallet } from '~/contracts/abi';

View File

@ -17,6 +17,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { RadioButtons } from '~/ui'; import { RadioButtons } from '~/ui';
import { walletSourceURL } from '~/contracts/code/wallet';
// import styles from '../createWallet.css'; // import styles from '../createWallet.css';
@ -46,7 +47,9 @@ export default class WalletType extends Component {
description: ( description: (
<span> <span>
<span>Create/Deploy a </span> <span>Create/Deploy a </span>
<a href='https://github.com/ethereum/dapp-bin/blob/master/wallet/wallet.sol' target='_blank'>standard multi-signature </a> <a href={ walletSourceURL } target='_blank'>
standard multi-signature
</a>
<span> Wallet</span> <span> Wallet</span>
</span> </span>
) )

View File

@ -16,13 +16,14 @@
import { observable, computed, action, transaction } from 'mobx'; import { observable, computed, action, transaction } from 'mobx';
import { validateUint, validateAddress, validateName } from '~/util/validation';
import { ERROR_CODES } from '~/api/transport/error';
import Contract from '~/api/contract'; import Contract from '~/api/contract';
import Contracts from '~/contracts';
import { ERROR_CODES } from '~/api/transport/error';
import { wallet as walletAbi } from '~/contracts/abi'; import { wallet as walletAbi } from '~/contracts/abi';
import { wallet as walletCode } from '~/contracts/code'; import { wallet as walletCode, walletLibraryRegKey, fullWalletCode } from '~/contracts/code/wallet';
import { validateUint, validateAddress, validateName } from '~/util/validation';
import { toWei } from '~/api/util/wei';
import WalletsUtils from '~/util/wallets'; import WalletsUtils from '~/util/wallets';
const STEPS = { const STEPS = {
@ -47,7 +48,7 @@ export default class CreateWalletStore {
address: '', address: '',
owners: [], owners: [],
required: 1, required: 1,
daylimit: 0, daylimit: toWei(1),
name: '', name: '',
description: '' description: ''
@ -160,14 +161,25 @@ export default class CreateWalletStore {
const { account, owners, required, daylimit } = this.wallet; const { account, owners, required, daylimit } = this.wallet;
Contracts
.get()
.registry
.lookupAddress(walletLibraryRegKey)
.then((address) => {
const walletLibraryAddress = (address || '').replace(/^0x/, '').toLowerCase();
const code = walletLibraryAddress.length && !/^0+$/.test(walletLibraryAddress)
? walletCode.replace(/(_)+WalletLibrary(_)+/g, walletLibraryAddress)
: fullWalletCode;
const options = { const options = {
data: walletCode, data: code,
from: account from: account
}; };
this.api return this.api
.newContract(walletAbi) .newContract(walletAbi)
.deploy(options, [ owners, required, daylimit ], this.onDeploymentState) .deploy(options, [ owners, required, daylimit ], this.onDeploymentState);
})
.then((address) => { .then((address) => {
this.deployed = true; this.deployed = true;
this.wallet.address = address; this.wallet.address = address;

View File

@ -18,8 +18,8 @@ import React, { Component, PropTypes } from 'react';
import { MenuItem } from 'material-ui'; import { MenuItem } from 'material-ui';
import { AddressSelect, Form, Input, Select } from '~/ui'; import { AddressSelect, Form, Input, Select } from '~/ui';
import { validateAbi } from '../../../util/validation'; import { validateAbi } from '~/util/validation';
import { parseAbiType } from '../../../util/abi'; import { parseAbiType } from '~/util/abi';
export default class DetailsStep extends Component { export default class DetailsStep extends Component {
static contextTypes = { static contextTypes = {

View File

@ -32,7 +32,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { Form, TypedInput } from '~/ui'; import { Form, TypedInput } from '~/ui';
import { parseAbiType } from '../../../util/abi'; import { parseAbiType } from '~/util/abi';
import styles from '../deployContract.css'; import styles from '../deployContract.css';

View File

@ -19,7 +19,7 @@ import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import { BusyStep, CompletedStep, CopyToClipboard, Button, IdentityIcon, Modal, TxHash } from '~/ui'; import { BusyStep, CompletedStep, CopyToClipboard, Button, IdentityIcon, Modal, TxHash } from '~/ui';
import { ERRORS, validateAbi, validateCode, validateName } from '../../util/validation'; import { ERRORS, validateAbi, validateCode, validateName } from '~/util/validation';
import DetailsStep from './DetailsStep'; import DetailsStep from './DetailsStep';
import ParametersStep from './ParametersStep'; import ParametersStep from './ParametersStep';

View File

@ -19,7 +19,7 @@ import ContentClear from 'material-ui/svg-icons/content/clear';
import ContentSave from 'material-ui/svg-icons/content/save'; import ContentSave from 'material-ui/svg-icons/content/save';
import { Button, Form, Input, InputChip, Modal } from '~/ui'; import { Button, Form, Input, InputChip, Modal } from '~/ui';
import { validateName } from '../../util/validation'; import { validateName } from '~/util/validation';
export default class EditMeta extends Component { export default class EditMeta extends Component {
static contextTypes = { static contextTypes = {

View File

@ -15,12 +15,19 @@
// 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 { Checkbox, MenuItem } from 'material-ui';
import { AddressSelect, Form, Input, InputAddressSelect, Select } from '~/ui'; import { AddressSelect, Form, Input, Select, TypedInput } from '~/ui';
import { parseAbiType } from '~/util/abi';
import styles from '../executeContract.css'; import styles from '../executeContract.css';
const CHECK_STYLE = {
position: 'absolute',
top: '38px',
left: '1em'
};
export default class DetailsStep extends Component { export default class DetailsStep extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
@ -30,10 +37,12 @@ export default class DetailsStep extends Component {
onAmountChange: PropTypes.func.isRequired, onAmountChange: PropTypes.func.isRequired,
fromAddress: PropTypes.string, fromAddress: PropTypes.string,
fromAddressError: PropTypes.string, fromAddressError: PropTypes.string,
gasEdit: PropTypes.bool,
onFromAddressChange: PropTypes.func.isRequired, onFromAddressChange: PropTypes.func.isRequired,
func: PropTypes.object, func: PropTypes.object,
funcError: PropTypes.string, funcError: PropTypes.string,
onFuncChange: PropTypes.func, onFuncChange: PropTypes.func,
onGasEditClick: PropTypes.func,
values: PropTypes.array.isRequired, values: PropTypes.array.isRequired,
valuesError: PropTypes.array.isRequired, valuesError: PropTypes.array.isRequired,
warning: PropTypes.string, warning: PropTypes.string,
@ -41,7 +50,7 @@ export default class DetailsStep extends Component {
} }
render () { render () {
const { accounts, amount, amountError, fromAddress, fromAddressError, onFromAddressChange, onAmountChange } = this.props; const { accounts, amount, amountError, fromAddress, fromAddressError, gasEdit, onGasEditClick, onFromAddressChange, onAmountChange } = this.props;
return ( return (
<Form> <Form>
@ -55,12 +64,23 @@ export default class DetailsStep extends Component {
onChange={ onFromAddressChange } /> onChange={ onFromAddressChange } />
{ this.renderFunctionSelect() } { this.renderFunctionSelect() }
{ this.renderParameters() } { this.renderParameters() }
<div className={ styles.columns }>
<div>
<Input <Input
label='transaction value (in ETH)' label='transaction value (in ETH)'
hint='the amount to send to with the transaction' hint='the amount to send to with the transaction'
value={ amount } value={ amount }
error={ amountError } error={ amountError }
onSubmit={ onAmountChange } /> onSubmit={ onAmountChange } />
</div>
<div>
<Checkbox
checked={ gasEdit }
label='edit gas price or value'
onCheck={ onGasEditClick }
style={ CHECK_STYLE } />
</div>
</div>
</Form> </Form>
); );
} }
@ -74,7 +94,7 @@ export default class DetailsStep extends Component {
const functions = contract.functions const functions = contract.functions
.filter((func) => !func.constant) .filter((func) => !func.constant)
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => (a.name || '').localeCompare(b.name || ''))
.map((func) => { .map((func) => {
const params = (func.abi.inputs || []) const params = (func.abi.inputs || [])
.map((input, index) => { .map((input, index) => {
@ -125,56 +145,22 @@ export default class DetailsStep extends Component {
} }
return (func.abi.inputs || []).map((input, index) => { return (func.abi.inputs || []).map((input, index) => {
const onChange = (event, value) => onValueChange(event, index, value); const onChange = (value) => onValueChange(null, index, value);
const onSelect = (event, _index, value) => onValueChange(event, index, value);
const onSubmit = (value) => onValueChange(null, index, value);
const label = `${input.name}: ${input.type}`; const label = `${input.name}: ${input.type}`;
let inputbox;
switch (input.type) { return (
case 'address': <div
inputbox = ( key={ `${index}_${input.name || ''}` }
<InputAddressSelect className={ styles.funcparams }
>
<TypedInput
label={ label }
value={ values[index] }
error={ valuesError[index] }
onChange={ onChange }
accounts={ accounts } accounts={ accounts }
editing param={ parseAbiType(input.type) }
label={ label } />
value={ values[index] }
error={ valuesError[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={ values[index] ? 'true' : 'false' }
error={ valuesError[index] }
onChange={ onSelect }>{ boolitems }</Select>
);
break;
default:
inputbox = (
<Input
label={ label }
value={ values[index] }
error={ valuesError[index] }
onSubmit={ onSubmit } />
);
}
return (
<div className={ styles.funcparams } key={ index }>
{ inputbox }
</div> </div>
); );
}); });

View File

@ -42,3 +42,15 @@
padding: 0.75em; padding: 0.75em;
text-align: center; text-align: center;
} }
.columns {
display: flex;
flex-wrap: wrap;
position: relative;
&>div {
flex: 0 1 50%;
width: 50%;
position: relative;
}
}

View File

@ -17,18 +17,36 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { observer } from 'mobx-react';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash } from '~/ui'; import { BusyStep, Button, CompletedStep, GasPriceEditor, IdentityIcon, Modal, TxHash } from '~/ui';
import { MAX_GAS_ESTIMATION } from '../../util/constants'; import { MAX_GAS_ESTIMATION } from '~/util/constants';
import { validateAddress, validateUint } from '../../util/validation'; import { validateAddress, validateUint } from '~/util/validation';
import { parseAbiType } from '~/util/abi';
import DetailsStep from './DetailsStep'; import DetailsStep from './DetailsStep';
import ERRORS from '../Transfer/errors';
import { ERROR_CODES } from '~/api/transport/error'; import { ERROR_CODES } from '~/api/transport/error';
const STEP_DETAILS = 0;
const STEP_BUSY_OR_GAS = 1;
const STEP_BUSY = 2;
const TITLES = {
transfer: 'function details',
sending: 'sending',
complete: 'complete',
gas: 'gas selection',
rejected: 'rejected'
};
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
const STAGES_GAS = [TITLES.transfer, TITLES.gas, TITLES.sending, TITLES.complete];
@observer
class ExecuteContract extends Component { class ExecuteContract extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired, api: PropTypes.object.isRequired,
@ -45,28 +63,29 @@ class ExecuteContract extends Component {
onFromAddressChange: PropTypes.func.isRequired onFromAddressChange: PropTypes.func.isRequired
} }
gasStore = new GasPriceEditor.Store(this.context.api, this.props.gasLimit);
state = { state = {
amount: '0', amount: '0',
amountError: null, amountError: null,
busyState: null,
fromAddressError: null, fromAddressError: null,
func: null, func: null,
funcError: null, funcError: null,
gas: null, gasEdit: false,
gasLimitError: null, rejected: false,
step: STEP_DETAILS,
sending: false,
values: [], values: [],
valuesError: [], valuesError: [],
step: 0, txhash: null
sending: false,
busyState: null,
txhash: null,
rejected: false
} }
componentDidMount () { componentDidMount () {
const { contract } = this.props; const { contract } = this.props;
const functions = contract.functions const functions = contract.functions
.filter((func) => !func.constant) .filter((func) => !func.constant)
.sort((a, b) => a.name.localeCompare(b.name)); .sort((a, b) => (a.name || '').localeCompare(b.name || ''));
this.onFuncChange(null, functions[0]); this.onFuncChange(null, functions[0]);
} }
@ -78,15 +97,21 @@ class ExecuteContract extends Component {
} }
render () { render () {
const { sending } = this.state; const { sending, step, gasEdit, rejected } = this.state;
const steps = gasEdit ? STAGES_GAS : STAGES_BASIC;
if (rejected) {
steps[steps.length - 1] = TITLES.rejected;
}
return ( return (
<Modal <Modal
actions={ this.renderDialogActions() } actions={ this.renderDialogActions() }
title='execute function'
busy={ sending } busy={ sending }
waiting={ [1] } current={ step }
visible> steps={ steps }
visible
waiting={ gasEdit ? [STEP_BUSY] : [STEP_BUSY_OR_GAS] }>
{ this.renderStep() } { this.renderStep() }
</Modal> </Modal>
); );
@ -94,7 +119,7 @@ class ExecuteContract extends Component {
renderDialogActions () { renderDialogActions () {
const { onClose, fromAddress } = this.props; const { onClose, fromAddress } = this.props;
const { sending, step, fromAddressError, valuesError } = this.state; const { gasEdit, sending, step, fromAddressError, valuesError } = this.state;
const hasError = fromAddressError || valuesError.find((error) => error); const hasError = fromAddressError || valuesError.find((error) => error);
const cancelBtn = ( const cancelBtn = (
@ -104,21 +129,44 @@ class ExecuteContract extends Component {
icon={ <ContentClear /> } icon={ <ContentClear /> }
onClick={ onClose } /> onClick={ onClose } />
); );
const postBtn = (
if (step === 0) {
return [
cancelBtn,
<Button <Button
key='postTransaction' key='postTransaction'
label='post transaction' label='post transaction'
disabled={ sending || hasError } disabled={ !!(sending || hasError) }
icon={ <IdentityIcon address={ fromAddress } button /> } icon={ <IdentityIcon address={ fromAddress } button /> }
onClick={ this.postTransaction } /> onClick={ this.postTransaction } />
);
const nextBtn = (
<Button
key='nextStep'
label='next'
icon={ <NavigationArrowForward /> }
onClick={ this.onNextClick } />
);
const prevBtn = (
<Button
key='prevStep'
label='prev'
icon={ <NavigationArrowBack /> }
onClick={ this.onPrevClick } />
);
if (step === STEP_DETAILS) {
return [
cancelBtn,
gasEdit ? nextBtn : postBtn
]; ];
} else if (step === 1) { } else if (step === (gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS)) {
return [ return [
cancelBtn cancelBtn
]; ];
} else if (gasEdit && (step === STEP_BUSY_OR_GAS)) {
return [
cancelBtn,
prevBtn,
postBtn
];
} }
return [ return [
@ -132,7 +180,8 @@ class ExecuteContract extends Component {
renderStep () { renderStep () {
const { onFromAddressChange } = this.props; const { onFromAddressChange } = this.props;
const { step, busyState, gasLimitError, txhash, rejected } = this.state; const { gasEdit, step, busyState, txhash, rejected } = this.state;
const { errorEstimated } = this.gasStore;
if (rejected) { if (rejected) {
return ( return (
@ -143,23 +192,29 @@ class ExecuteContract extends Component {
); );
} }
if (step === 0) { if (step === STEP_DETAILS) {
return ( return (
<DetailsStep <DetailsStep
{ ...this.props } { ...this.props }
{ ...this.state } { ...this.state }
warning={ gasLimitError } warning={ errorEstimated }
onAmountChange={ this.onAmountChange } onAmountChange={ this.onAmountChange }
onFromAddressChange={ onFromAddressChange } onFromAddressChange={ onFromAddressChange }
onFuncChange={ this.onFuncChange } onFuncChange={ this.onFuncChange }
onGasEditClick={ this.onGasEditClick }
onValueChange={ this.onValueChange } /> onValueChange={ this.onValueChange } />
); );
} else if (step === 1) { } else if (step === (gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS)) {
return ( return (
<BusyStep <BusyStep
title='The function execution is in progress' title='The function execution is in progress'
state={ busyState } /> state={ busyState } />
); );
} else if (gasEdit && (step === STEP_BUSY_OR_GAS)) {
return (
<GasPriceEditor
store={ this.gasStore } />
);
} }
return ( return (
@ -170,27 +225,14 @@ class ExecuteContract extends Component {
} }
onAmountChange = (amount) => { onAmountChange = (amount) => {
this.gasStore.setEthValue(amount);
this.setState({ amount }, this.estimateGas); this.setState({ amount }, this.estimateGas);
} }
onFuncChange = (event, func) => { onFuncChange = (event, func) => {
const values = func.inputs.map((input) => { const values = (func.abi.inputs || []).map((input) => {
switch (input.kind.type) { const parsedType = parseAbiType(input.type);
case 'address': return parsedType.default;
return '0x';
case 'bool':
return false;
case 'bytes':
return '0x';
case 'uint':
return '0';
default:
return '';
}
}); });
this.setState({ this.setState({
@ -234,7 +276,7 @@ class ExecuteContract extends Component {
estimateGas = (_fromAddress) => { estimateGas = (_fromAddress) => {
const { api } = this.context; const { api } = this.context;
const { fromAddress, gasLimit } = this.props; const { fromAddress } = this.props;
const { amount, func, values } = this.state; const { amount, func, values } = this.state;
const options = { const options = {
gas: MAX_GAS_ESTIMATION, gas: MAX_GAS_ESTIMATION,
@ -250,18 +292,11 @@ class ExecuteContract extends Component {
.estimateGas(options, values) .estimateGas(options, values)
.then((gasEst) => { .then((gasEst) => {
const gas = gasEst.mul(1.2); const gas = gasEst.mul(1.2);
let gasLimitError = null;
if (gas.gte(MAX_GAS_ESTIMATION)) { console.log(`estimateGas: received ${gasEst.toFormat(0)}, adjusted to ${gas.toFormat(0)}`);
gasLimitError = ERRORS.gasException;
} else if (gas.gt(gasLimit)) {
gasLimitError = ERRORS.gasBlockLimit;
}
this.setState({ this.gasStore.setEstimated(gasEst.toFixed(0));
gas, this.gasStore.setGas(gas.toFixed(0));
gasLimitError
});
}) })
.catch((error) => { .catch((error) => {
console.warn('estimateGas', error); console.warn('estimateGas', error);
@ -271,22 +306,20 @@ class ExecuteContract extends Component {
postTransaction = () => { postTransaction = () => {
const { api, store } = this.context; const { api, store } = this.context;
const { fromAddress } = this.props; const { fromAddress } = this.props;
const { amount, func, values } = this.state; const { amount, func, gasEdit, values } = this.state;
const steps = gasEdit ? STAGES_GAS : STAGES_BASIC;
const finalstep = steps.length - 1;
const options = { const options = {
gas: MAX_GAS_ESTIMATION, gas: this.gasStore.gas,
gasPrice: this.gasStore.price,
from: fromAddress, from: fromAddress,
value: api.util.toWei(amount || 0) value: api.util.toWei(amount || 0)
}; };
this.setState({ sending: true, step: 1 }); this.setState({ sending: true, step: gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS });
func func
.estimateGas(options, values) .postTransaction(options, values)
.then((gas) => {
options.gas = gas.mul(1.2).toFixed(0);
console.log(`estimateGas: received ${gas.toFormat(0)}, adjusted to ${gas.mul(1.2).toFormat(0)}`);
return func.postTransaction(options, values);
})
.then((requestId) => { .then((requestId) => {
this.setState({ busyState: 'Waiting for authorization in the Parity Signer' }); this.setState({ busyState: 'Waiting for authorization in the Parity Signer' });
@ -294,7 +327,7 @@ class ExecuteContract extends Component {
.pollMethod('parity_checkRequest', requestId) .pollMethod('parity_checkRequest', requestId)
.catch((error) => { .catch((error) => {
if (error.code === ERROR_CODES.REQUEST_REJECTED) { if (error.code === ERROR_CODES.REQUEST_REJECTED) {
this.setState({ rejected: true }); this.setState({ rejected: true, step: finalstep });
return false; return false;
} }
@ -302,13 +335,31 @@ class ExecuteContract extends Component {
}); });
}) })
.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: finalstep, txhash, busyState: 'Your transaction has been posted to the network' });
}) })
.catch((error) => { .catch((error) => {
console.error('postTransaction', error); console.error('postTransaction', error);
store.dispatch({ type: 'newError', error }); store.dispatch({ type: 'newError', error });
}); });
} }
onGasEditClick = () => {
this.setState({
gasEdit: !this.state.gasEdit
});
}
onNextClick = () => {
this.setState({
step: this.state.step + 1
});
}
onPrevClick = () => {
this.setState({
step: this.state.step - 1
});
}
} }
function mapStateToProps (state) { function mapStateToProps (state) {

View File

@ -21,9 +21,8 @@ import { sha3 } from '~/api/util/sha3';
import Contracts from '~/contracts'; import Contracts from '~/contracts';
import { checkIfVerified, checkIfRequested, awaitPuzzle } from '~/contracts/sms-verification'; import { checkIfVerified, checkIfRequested, awaitPuzzle } from '~/contracts/sms-verification';
import { postToServer } from '../../3rdparty/sms-verification'; import { postToServer } from '~/3rdparty/sms-verification';
import checkIfTxFailed from '../../util/check-if-tx-failed'; import { checkIfTxFailed, waitForConfirmations } from '~/util/tx';
import waitForConfirmations from '../../util/wait-for-block-confirmations';
export const LOADING = 'fetching-contract'; export const LOADING = 'fetching-contract';
export const QUERY_DATA = 'query-data'; export const QUERY_DATA = 'query-data';

View File

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

View File

@ -19,7 +19,7 @@ import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import { Button, IdentityIcon, Modal } from '~/ui'; import { Button, IdentityIcon, Modal } from '~/ui';
import initShapeshift from '../../3rdparty/shapeshift'; import initShapeshift from '~/3rdparty/shapeshift';
import shapeshiftLogo from '../../../assets/images/shapeshift-logo.png'; import shapeshiftLogo from '../../../assets/images/shapeshift-logo.png';
import AwaitingDepositStep from './AwaitingDepositStep'; import AwaitingDepositStep from './AwaitingDepositStep';

View File

@ -16,96 +16,28 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import Form, { Input } from '~/ui/Form'; import { GasPriceEditor, Form, Input } from '~/ui';
import GasPriceSelector from '../GasPriceSelector';
import styles from '../transfer.css';
export default class Extras extends Component { export default class Extras extends Component {
static propTypes = { static propTypes = {
isEth: PropTypes.bool, isEth: PropTypes.bool,
data: PropTypes.string, data: PropTypes.string,
dataError: PropTypes.string, dataError: PropTypes.string,
gas: PropTypes.string,
gasEst: PropTypes.string,
gasError: PropTypes.string,
gasPrice: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object
]),
gasPriceDefault: PropTypes.string,
gasPriceError: PropTypes.string,
gasPriceHistogram: PropTypes.object,
total: PropTypes.string, total: PropTypes.string,
totalError: PropTypes.string, totalError: PropTypes.string,
onChange: PropTypes.func.isRequired onChange: PropTypes.func.isRequired,
gasStore: PropTypes.object.isRequired
} }
render () { render () {
const { gas, gasPrice, gasError, gasEst, gasPriceDefault, gasPriceError, gasPriceHistogram, total, totalError } = this.props; const { gasStore, onChange } = this.props;
const gasLabel = `gas amount (estimated: ${gasEst})`;
const priceLabel = `gas price (current: ${gasPriceDefault})`;
return ( return (
<Form> <Form>
{ this.renderData() } { this.renderData() }
<GasPriceEditor
<div className={ styles.columns }> store={ gasStore }
<div style={ { flex: 65 } }> onChange={ onChange } />
<GasPriceSelector
gasPriceHistogram={ gasPriceHistogram }
gasPrice={ gasPrice }
onChange={ this.onEditGasPrice }
/>
</div>
<div
className={ styles.row }
style={ {
flex: 35, paddingLeft: '1rem',
justifyContent: 'space-around',
paddingBottom: 12
} }
>
<div className={ styles.row }>
<Input
label={ gasLabel }
hint='the amount of gas to use for the transaction'
error={ gasError }
value={ gas }
onChange={ this.onEditGas } />
<Input
label={ priceLabel }
hint='the price of gas to use for the transaction'
error={ gasPriceError }
value={ (gasPrice || '').toString() }
onChange={ this.onEditGasPrice } />
</div>
<div className={ styles.row }>
<Input
disabled
label='total transaction amount'
hint='the total amount of the transaction'
error={ totalError }
value={ `${total} ETH` } />
</div>
</div>
</div>
<div>
<p className={ styles.gasPriceDesc }>
You can choose the gas price based on the
distribution of recent included transactions' gas prices.
The lower the gas price is, the cheaper the transaction will
be. The higher the gas price is, the faster it should
get mined by the network.
</p>
</div>
</Form> </Form>
); );
} }
@ -129,14 +61,6 @@ export default class Extras extends Component {
); );
} }
onEditGas = (event) => {
this.props.onChange('gas', event.target.value);
}
onEditGasPrice = (event, value) => {
this.props.onChange('gasPrice', value);
}
onEditData = (event) => { onEditData = (event) => {
this.props.onChange('data', event.target.value); this.props.onChange('data', event.target.value);
} }

View File

@ -23,7 +23,8 @@ import { bytesToHex } from '~/api/util/format';
import Contract from '~/api/contract'; import Contract from '~/api/contract';
import ERRORS from './errors'; import ERRORS from './errors';
import { ERROR_CODES } from '~/api/transport/error'; import { ERROR_CODES } from '~/api/transport/error';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants'; import { DEFAULT_GAS, MAX_GAS_ESTIMATION } from '~/util/constants';
import GasPriceStore from '~/ui/GasPriceEditor/store';
const TITLES = { const TITLES = {
transfer: 'transfer details', transfer: 'transfer details',
@ -48,14 +49,6 @@ export default class TransferStore {
@observable data = ''; @observable data = '';
@observable dataError = null; @observable dataError = null;
@observable gas = DEFAULT_GAS;
@observable gasError = null;
@observable gasEst = '0';
@observable gasLimitError = null;
@observable gasPrice = DEFAULT_GASPRICE;
@observable gasPriceError = null;
@observable recipient = ''; @observable recipient = '';
@observable recipientError = ERRORS.requireRecipient; @observable recipientError = ERRORS.requireRecipient;
@ -68,11 +61,8 @@ export default class TransferStore {
@observable value = '0.0'; @observable value = '0.0';
@observable valueError = null; @observable valueError = null;
gasPriceHistogram = {};
account = null; account = null;
balance = null; balance = null;
gasLimit = null;
onClose = null; onClose = null;
senders = null; senders = null;
@ -81,6 +71,8 @@ export default class TransferStore {
isWallet = false; isWallet = false;
wallet = null; wallet = null;
gasStore = null;
@computed get steps () { @computed get steps () {
const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC); const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC);
@ -93,7 +85,7 @@ export default class TransferStore {
@computed get isValid () { @computed get isValid () {
const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError; const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError;
const extrasValid = !this.gasError && !this.gasPriceError && !this.totalError; const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.totalError;
const verifyValid = !this.passwordError; const verifyValid = !this.passwordError;
switch (this.stage) { switch (this.stage) {
@ -115,14 +107,14 @@ export default class TransferStore {
constructor (api, props) { constructor (api, props) {
this.api = api; this.api = api;
const { account, balance, gasLimit, senders, onClose, newError, sendersBalances } = props; const { account, balance, gasLimit, senders, newError, sendersBalances } = props;
this.account = account; this.account = account;
this.balance = balance; this.balance = balance;
this.gasLimit = gasLimit;
this.onClose = onClose;
this.isWallet = account && account.wallet; this.isWallet = account && account.wallet;
this.newError = newError; this.newError = newError;
this.gasStore = new GasPriceStore(api, gasLimit);
if (this.isWallet) { if (this.isWallet) {
this.wallet = props.wallet; this.wallet = props.wallet;
this.walletContract = new Contract(this.api, walletAbi); this.walletContract = new Contract(this.api, walletAbi);
@ -143,8 +135,7 @@ export default class TransferStore {
this.stage -= 1; this.stage -= 1;
} }
@action onClose = () => { @action handleClose = () => {
this.onClose && this.onClose();
this.stage = 0; this.stage = 0;
} }
@ -179,26 +170,6 @@ export default class TransferStore {
} }
} }
@action getDefaults = () => {
Promise
.all([
this.api.parity.gasPriceHistogram(),
this.api.eth.gasPrice()
])
.then(([gasPriceHistogram, gasPrice]) => {
transaction(() => {
this.gasPrice = gasPrice.toString();
this.gasPriceDefault = gasPrice.toFormat();
this.gasPriceHistogram = gasPriceHistogram;
this.recalculate();
});
})
.catch((error) => {
console.warn('getDefaults', error);
});
}
@action onSend = () => { @action onSend = () => {
this.onNext(); this.onNext();
this.sending = true; this.sending = true;
@ -281,25 +252,11 @@ export default class TransferStore {
} }
@action _onUpdateGas = (gas) => { @action _onUpdateGas = (gas) => {
const gasError = this._validatePositiveNumber(gas);
transaction(() => {
this.gas = gas;
this.gasError = gasError;
this.recalculate(); this.recalculate();
});
} }
@action _onUpdateGasPrice = (gasPrice) => { @action _onUpdateGasPrice = (gasPrice) => {
const gasPriceError = this._validatePositiveNumber(gasPrice);
transaction(() => {
this.gasPrice = gasPrice;
this.gasPriceError = gasPriceError;
this.recalculate(); this.recalculate();
});
} }
@action _onUpdateRecipient = (recipient) => { @action _onUpdateRecipient = (recipient) => {
@ -362,7 +319,7 @@ export default class TransferStore {
@action recalculateGas = () => { @action recalculateGas = () => {
if (!this.isValid) { if (!this.isValid) {
this.gas = 0; this.gasStore.setGas('0');
return this.recalculate(); return this.recalculate();
} }
@ -370,28 +327,20 @@ export default class TransferStore {
.estimateGas() .estimateGas()
.then((gasEst) => { .then((gasEst) => {
let gas = gasEst; let gas = gasEst;
let gasLimitError = null;
if (gas.gt(DEFAULT_GAS)) { if (gas.gt(DEFAULT_GAS)) {
gas = gas.mul(1.2); gas = gas.mul(1.2);
} }
if (gas.gte(MAX_GAS_ESTIMATION)) {
gasLimitError = ERRORS.gasException;
} else if (gas.gt(this.gasLimit)) {
gasLimitError = ERRORS.gasBlockLimit;
}
transaction(() => { transaction(() => {
this.gas = gas.toFixed(0); this.gasStore.setEstimated(gasEst.toFixed(0));
this.gasEst = gasEst.toFormat(); this.gasStore.setGas(gas.toFixed(0));
this.gasLimitError = gasLimitError;
this.recalculate(); this.recalculate();
}); });
}) })
.catch((error) => { .catch((error) => {
console.error('etimateGas', error); console.warn('etimateGas', error);
this.recalculate(); this.recalculate();
}); });
} }
@ -411,9 +360,9 @@ export default class TransferStore {
return; return;
} }
const { gas, gasPrice, tag, valueAll, isEth, isWallet } = this; const { tag, valueAll, isEth, isWallet } = this;
const gasTotal = new BigNumber(gasPrice || 0).mul(new BigNumber(gas || 0)); const gasTotal = new BigNumber(this.gasStore.price || 0).mul(new BigNumber(this.gasStore.gas || 0));
const availableEth = new BigNumber(balance.tokens[0].value); const availableEth = new BigNumber(balance.tokens[0].value);
@ -453,10 +402,12 @@ export default class TransferStore {
} }
transaction(() => { transaction(() => {
this.total = this.api.util.fromWei(totalEth).toString(); this.total = this.api.util.fromWei(totalEth).toFixed();
this.totalError = totalError; this.totalError = totalError;
this.value = value; this.value = value;
this.valueError = valueError; this.valueError = valueError;
this.gasStore.setErrorTotal(totalError);
this.gasStore.setEthValue(totalEth);
}); });
} }
@ -522,8 +473,8 @@ export default class TransferStore {
}; };
if (!gas) { if (!gas) {
options.gas = this.gas; options.gas = this.gasStore.gas;
options.gasPrice = this.gasPrice; options.gasPrice = this.gasStore.price;
} else { } else {
options.gas = MAX_GAS_ESTIMATION; options.gas = MAX_GAS_ESTIMATION;
} }

View File

@ -144,15 +144,6 @@
font-size: 1.2rem; font-size: 1.2rem;
} }
.chart {
position: absolute;
width: 100%;
}
.gasPriceDesc {
font-size: 0.9em;
}
.warning { .warning {
border-radius: 0.5em; border-radius: 0.5em;
background: #f80; background: #f80;

View File

@ -56,10 +56,6 @@ class Transfer extends Component {
store = new TransferStore(this.context.api, this.props); store = new TransferStore(this.context.api, this.props);
componentDidMount () {
this.store.getDefaults();
}
render () { render () {
const { stage, extras, steps } = this.store; const { stage, extras, steps } = this.store;
@ -186,27 +182,20 @@ class Transfer extends Component {
} }
renderExtrasPage () { renderExtrasPage () {
if (!this.store.gasPriceHistogram) { if (!this.store.gasStore.histogram) {
return null; return null;
} }
const { isEth, data, dataError, gas, gasEst, gasError, gasPrice } = this.store; const { isEth, data, dataError, total, totalError } = this.store;
const { gasPriceDefault, gasPriceError, gasPriceHistogram, total, totalError } = this.store;
return ( return (
<Extras <Extras
isEth={ isEth } isEth={ isEth }
data={ data } data={ data }
dataError={ dataError } dataError={ dataError }
gas={ gas }
gasEst={ gasEst }
gasError={ gasError }
gasPrice={ gasPrice }
gasPriceDefault={ gasPriceDefault }
gasPriceError={ gasPriceError }
gasPriceHistogram={ gasPriceHistogram }
total={ total } total={ total }
totalError={ totalError } totalError={ totalError }
gasStore={ this.store.gasStore }
onChange={ this.store.onUpdateDetails } /> onChange={ this.store.onUpdateDetails } />
); );
} }
@ -219,7 +208,7 @@ class Transfer extends Component {
<Button <Button
icon={ <ContentClear /> } icon={ <ContentClear /> }
label='Cancel' label='Cancel'
onClick={ this.store.onClose } /> onClick={ this.handleClose } />
); );
const nextBtn = ( const nextBtn = (
<Button <Button
@ -245,7 +234,7 @@ class Transfer extends Component {
<Button <Button
icon={ <ActionDoneAll /> } icon={ <ActionDoneAll /> }
label='Close' label='Close'
onClick={ this.store.onClose } /> onClick={ this.handleClose } />
); );
switch (stage) { switch (stage) {
@ -263,18 +252,25 @@ class Transfer extends Component {
} }
renderWarning () { renderWarning () {
const { gasLimitError } = this.store; const { errorEstimated } = this.store.gasStore;
if (!gasLimitError) { if (!errorEstimated) {
return null; return null;
} }
return ( return (
<div className={ styles.warning }> <div className={ styles.warning }>
{ gasLimitError } { errorEstimated }
</div> </div>
); );
} }
handleClose = () => {
const { onClose } = this.props;
this.store.handleClose();
typeof onClose === 'function' && onClose();
}
} }
function mapStateToProps (initState, initProps) { function mapStateToProps (initState, initProps) {

View File

@ -92,7 +92,7 @@ function findImports (path) {
return { error: 'File not found' }; return { error: 'File not found' };
} }
function compile (data) { function compile (data, optimized = 1) {
const { sourcecode, build } = data; const { sourcecode, build } = data;
const { longVersion } = build; const { longVersion } = build;
@ -109,7 +109,7 @@ function compile (data) {
'': sourcecode '': sourcecode
}; };
const compiled = compiler.compile({ sources: input }, 0, findImports); const compiled = compiler.compile({ sources: input }, optimized, findImports);
self.lastCompile = { self.lastCompile = {
version: longVersion, result: compiled, version: longVersion, result: compiled,

View File

@ -36,9 +36,6 @@ export default class Personal {
} }
this._store.dispatch(personalAccountsInfo(accountsInfo)); this._store.dispatch(personalAccountsInfo(accountsInfo));
})
.then((subscriptionId) => {
console.log('personal._subscribeAccountsInfo', 'subscriptionId', subscriptionId);
}); });
} }

View File

@ -34,9 +34,6 @@ export default class Signer {
} }
this._store.dispatch(signerRequestsToConfirm(pending || [])); this._store.dispatch(signerRequestsToConfirm(pending || []));
})
.then((subscriptionId) => {
console.log('signer._subscribeRequestsToConfirm', 'subscriptionId', subscriptionId);
}); });
} }
} }

View File

@ -59,9 +59,6 @@ export default class Status {
.catch((error) => { .catch((error) => {
console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error); console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error);
}); });
})
.then((subscriptionId) => {
console.log('status._subscribeBlockNumber', 'subscriptionId', subscriptionId);
}); });
} }

View File

@ -17,12 +17,10 @@
import { isEqual, uniq } from 'lodash'; import { isEqual, uniq } from 'lodash';
import Contract from '~/api/contract'; import Contract from '~/api/contract';
import { wallet as WALLET_ABI } from '~/contracts/abi';
import { bytesToHex, toHex } from '~/api/util/format'; import { bytesToHex, toHex } from '~/api/util/format';
import { ERROR_CODES } from '~/api/transport/error'; import { ERROR_CODES } from '~/api/transport/error';
import { MAX_GAS_ESTIMATION } from '../../util/constants'; import { wallet as WALLET_ABI } from '~/contracts/abi';
import { MAX_GAS_ESTIMATION } from '~/util/constants';
import WalletsUtils from '~/util/wallets'; import WalletsUtils from '~/util/wallets';
import { newError } from '~/ui/Errors/actions'; import { newError } from '~/ui/Errors/actions';
@ -58,7 +56,7 @@ function modifyOperation (method, address, owner, operation) {
contract.instance[method] contract.instance[method]
.estimateGas(options, values) .estimateGas(options, values)
.then((gas) => { .then((gas) => {
options.gas = gas; options.gas = gas.mul(1.2);
return contract.instance[method].postTransaction(options, values); return contract.instance[method].postTransaction(options, values);
}) })
.then((requestId) => { .then((requestId) => {

View File

@ -0,0 +1,38 @@
// 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';
import { shallow } from 'enzyme';
import Actionbar from './actionbar';
function renderShallow (props) {
return shallow(
<Actionbar { ...props } />
);
}
describe('ui/Actionbar', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(renderShallow()).to.be.ok;
});
it('renders with the specified className', () => {
expect(renderShallow({ className: 'testClass' })).to.have.className('testClass');
});
});
});

View File

@ -14,15 +14,25 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
const checkIfTxFailed = (api, tx, gasSent) => { import React from 'react';
return api.pollMethod('eth_getTransactionReceipt', tx) import { shallow } from 'enzyme';
.then((receipt) => {
// TODO: Right now, there's no way to tell wether the EVM code crashed.
// Because you usually send a bit more gas than estimated (to make sure
// it gets mined quickly), we transaction probably failed if all the gas
// has been used up.
return receipt.gasUsed.eq(gasSent);
});
};
export default checkIfTxFailed; import Badge from './badge';
function renderShallow (props) {
return shallow(
<Badge { ...props } />
);
}
describe('ui/Badge', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(renderShallow()).to.be.ok;
});
it('renders with the specified className', () => {
expect(renderShallow({ className: 'testClass' })).to.have.className('testClass');
});
});
});

View File

@ -17,16 +17,15 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FlatButton } from 'material-ui'; import { FlatButton } from 'material-ui';
import { nodeOrStringProptype } from '~/util/proptypes';
export default class Button extends Component { export default class Button extends Component {
static propTypes = { static propTypes = {
backgroundColor: PropTypes.string, backgroundColor: PropTypes.string,
className: PropTypes.string, className: PropTypes.string,
disabled: PropTypes.bool, disabled: PropTypes.bool,
icon: PropTypes.node, icon: PropTypes.node,
label: PropTypes.oneOfType([ label: nodeOrStringProptype(),
React.PropTypes.string,
React.PropTypes.object
]),
onClick: PropTypes.func, onClick: PropTypes.func,
primary: PropTypes.bool primary: PropTypes.bool
} }

View File

@ -0,0 +1,38 @@
// 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';
import { shallow } from 'enzyme';
import Button from './button';
function renderShallow (props) {
return shallow(
<Button { ...props } />
);
}
describe('ui/Button', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(renderShallow()).to.be.ok;
});
it('renders with the specified className', () => {
expect(renderShallow({ className: 'testClass' })).to.have.className('testClass');
});
});
});

View File

@ -27,10 +27,6 @@ export default class Title extends Component {
byline: nodeOrStringProptype() byline: nodeOrStringProptype()
} }
state = {
name: 'Unnamed'
}
render () { render () {
const { className, title, byline } = this.props; const { className, title, byline } = this.props;

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 from 'react';
import { mount, shallow } from 'enzyme';
import Title from './title';
function renderShallow (props) {
return shallow(
<Title { ...props } />
);
}
function renderMount (props) {
return mount(
<Title { ...props } />
);
}
describe('ui/Container/Title', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(renderShallow()).to.be.ok;
});
it('renders with the specified className', () => {
expect(renderShallow({ className: 'testClass' })).to.have.className('testClass');
});
it('renders the specified title', () => {
expect(renderMount({ title: 'titleText' })).to.contain.text('titleText');
});
it('renders the specified byline', () => {
expect(renderMount({ byline: 'bylineText' })).to.contain.text('bylineText');
});
});
});

View File

@ -0,0 +1,38 @@
// 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';
import { shallow } from 'enzyme';
import Container from './container';
function renderShallow (props) {
return shallow(
<Container { ...props } />
);
}
describe('ui/Container', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(renderShallow()).to.be.ok;
});
it('renders with the specified className', () => {
expect(renderShallow({ className: 'testClass' })).to.have.className('testClass');
});
});
});

View File

@ -113,32 +113,38 @@ export default class Input extends Component {
<TextField <TextField
autoComplete='off' autoComplete='off'
className={ className } className={ className }
style={ textFieldStyle }
readOnly={ readOnly }
errorText={ error } errorText={ error }
floatingLabelFixed floatingLabelFixed
floatingLabelText={ label } floatingLabelText={ label }
fullWidth
hintText={ hint } hintText={ hint }
id={ NAME_ID }
inputStyle={ inputStyle }
fullWidth
max={ max }
min={ min }
multiLine={ multiLine } multiLine={ multiLine }
name={ NAME_ID } name={ NAME_ID }
id={ NAME_ID }
rows={ rows }
type={ type || 'text' }
underlineDisabledStyle={ UNDERLINE_DISABLED }
underlineStyle={ readOnly ? UNDERLINE_READONLY : UNDERLINE_NORMAL }
underlineFocusStyle={ readOnly ? { display: 'none' } : null }
underlineShow={ !hideUnderline }
value={ value }
onBlur={ this.onBlur } onBlur={ this.onBlur }
onChange={ this.onChange } onChange={ this.onChange }
onKeyDown={ this.onKeyDown } onKeyDown={ this.onKeyDown }
onPaste={ this.onPaste } onPaste={ this.onPaste }
inputStyle={ inputStyle }
min={ min } readOnly={ readOnly }
max={ max } rows={ rows }
style={ textFieldStyle }
type={ type || 'text' }
underlineDisabledStyle={ UNDERLINE_DISABLED }
underlineStyle={ readOnly ? UNDERLINE_READONLY : UNDERLINE_NORMAL }
underlineFocusStyle={ readOnly ? { display: 'none' } : null }
underlineShow={ !hideUnderline }
value={ value }
> >
{ children } { children }
</TextField> </TextField>

View File

@ -22,12 +22,11 @@ import IconButton from 'material-ui/IconButton';
import AddIcon from 'material-ui/svg-icons/content/add'; import AddIcon from 'material-ui/svg-icons/content/add';
import RemoveIcon from 'material-ui/svg-icons/content/remove'; import RemoveIcon from 'material-ui/svg-icons/content/remove';
import { fromWei, toWei } from '~/api/util/wei';
import Input from '~/ui/Form/Input'; import Input from '~/ui/Form/Input';
import InputAddressSelect from '~/ui/Form/InputAddressSelect'; import InputAddressSelect from '~/ui/Form/InputAddressSelect';
import Select from '~/ui/Form/Select'; import Select from '~/ui/Form/Select';
import { ABI_TYPES } from '~/util/abi'; import { ABI_TYPES } from '~/util/abi';
import { fromWei, toWei } from '~/api/util/wei';
import styles from './typedInput.css'; import styles from './typedInput.css';
@ -54,13 +53,13 @@ export default class TypedInput extends Component {
}; };
state = { state = {
isEth: true, isEth: false,
ethValue: 0 ethValue: 0
}; };
componentDidMount () { componentWillMount () {
if (this.props.isEth && this.props.value) { if (this.props.isEth && this.props.value) {
this.setState({ ethValue: fromWei(this.props.value) }); this.setState({ isEth: true, ethValue: fromWei(this.props.value) });
} }
} }
@ -165,28 +164,32 @@ export default class TypedInput extends Component {
} }
if (type === ABI_TYPES.INT) { if (type === ABI_TYPES.INT) {
return this.renderNumber(); return this.renderEth();
} }
if (type === ABI_TYPES.FIXED) { if (type === ABI_TYPES.FIXED) {
return this.renderNumber(); return this.renderFloat();
} }
return this.renderDefault(); return this.renderDefault();
} }
renderEth () { renderEth () {
const { ethValue } = this.state; const { ethValue, isEth } = this.state;
const value = ethValue && typeof ethValue.toNumber === 'function' const value = ethValue && typeof ethValue.toNumber === 'function'
? ethValue.toNumber() ? ethValue.toNumber()
: ethValue; : ethValue;
const input = isEth
? this.renderFloat(value, this.onEthValueChange)
: this.renderInteger(value, this.onEthValueChange);
return ( return (
<div className={ styles.ethInput }> <div className={ styles.ethInput }>
<div className={ styles.input }> <div className={ styles.input }>
{ this.renderNumber(value, this.onEthValueChange) } { input }
{ this.state.isEth ? (<div className={ styles.label }>ETH</div>) : null } { isEth ? (<div className={ styles.label }>ETH</div>) : null }
</div> </div>
<div className={ styles.toggle }> <div className={ styles.toggle }>
<Toggle <Toggle
@ -199,8 +202,9 @@ export default class TypedInput extends Component {
); );
} }
renderNumber (value = this.props.value, onChange = this.onChange) { renderInteger (value = this.props.value, onChange = this.onChange) {
const { label, error, param, hint, min, max } = this.props; const { label, error, param, hint, min, max } = this.props;
const realValue = value && typeof value.toNumber === 'function' const realValue = value && typeof value.toNumber === 'function'
? value.toNumber() ? value.toNumber()
: value; : value;
@ -213,6 +217,35 @@ export default class TypedInput extends Component {
error={ error } error={ error }
onChange={ onChange } onChange={ onChange }
type='number' type='number'
step={ 1 }
min={ min !== null ? min : (param.signed ? null : 0) }
max={ max !== null ? max : null }
/>
);
}
/**
* Decimal numbers have to be input via text field
* because of some react issues with input number fields.
* Once the issue is fixed, this could be a number again.
*
* @see https://github.com/facebook/react/issues/1549
*/
renderFloat (value = this.props.value, onChange = this.onChange) {
const { label, error, param, hint, min, max } = this.props;
const realValue = value && typeof value.toNumber === 'function'
? value.toNumber()
: value;
return (
<Input
label={ label }
hint={ hint }
value={ realValue }
error={ error }
onChange={ onChange }
type='text'
min={ min !== null ? min : (param.signed ? null : 0) } min={ min !== null ? min : (param.signed ? null : 0) }
max={ max !== null ? max : null } max={ max !== null ? max : null }
/> />

View File

@ -15,3 +15,13 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.chart {
position: absolute;
width: 100%;
}
.columns {
display: flex;
flex-wrap: wrap;
position: relative;
}

View File

@ -29,10 +29,7 @@ import {
import Slider from 'material-ui/Slider'; import Slider from 'material-ui/Slider';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import componentStyles from './gasPriceSelector.css'; import styles from './gasPriceSelector.css';
import mainStyles from '../transfer.css';
const styles = Object.assign({}, mainStyles, componentStyles);
const COLORS = { const COLORS = {
default: 'rgba(255, 99, 132, 0.2)', default: 'rgba(255, 99, 132, 0.2)',
@ -194,10 +191,7 @@ class CustomizedShape extends Component {
class CustomTooltip extends Component { class CustomTooltip extends Component {
static propTypes = { static propTypes = {
gasPriceHistogram: PropTypes.shape({ gasPriceHistogram: PropTypes.object.isRequired,
bucketBounds: PropTypes.array.isRequired,
counts: PropTypes.array.isRequired
}).isRequired,
type: PropTypes.string, type: PropTypes.string,
payload: PropTypes.array, payload: PropTypes.array,
label: PropTypes.number, label: PropTypes.number,
@ -231,12 +225,16 @@ class CustomTooltip extends Component {
} }
} }
const TOOL_STYLE = {
color: 'rgba(255,255,255,0.5)',
backgroundColor: 'rgba(0, 0, 0, 0.75)',
padding: '0 0.5em',
fontSize: '0.75em'
};
export default class GasPriceSelector extends Component { export default class GasPriceSelector extends Component {
static propTypes = { static propTypes = {
gasPriceHistogram: PropTypes.shape({ gasPriceHistogram: PropTypes.object.isRequired,
bucketBounds: PropTypes.array.isRequired,
counts: PropTypes.array.isRequired
}).isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
gasPrice: PropTypes.oneOfType([ gasPrice: PropTypes.oneOfType([
@ -287,7 +285,8 @@ export default class GasPriceSelector extends Component {
renderSlider () { renderSlider () {
const { sliderValue } = this.state; const { sliderValue } = this.state;
return (<div className={ styles.columns }> return (
<div className={ styles.columns }>
<Slider <Slider
min={ 0 } min={ 0 }
max={ 1 } max={ 1 }
@ -301,7 +300,8 @@ export default class GasPriceSelector extends Component {
marginBottom: 12 marginBottom: 12
} } } }
/> />
</div>); </div>
);
} }
renderChart () { renderChart () {
@ -316,7 +316,8 @@ export default class GasPriceSelector extends Component {
const countIndex = Math.max(0, Math.min(selectedIndex, gasPriceHistogram.counts.length - 1)); const countIndex = Math.max(0, Math.min(selectedIndex, gasPriceHistogram.counts.length - 1));
const selectedCount = countModifier(gasPriceHistogram.counts[countIndex]); const selectedCount = countModifier(gasPriceHistogram.counts[countIndex]);
return (<div className={ styles.columns }> return (
<div className={ styles.columns }>
<div style={ { flex: 1, height } }> <div style={ { flex: 1, height } }>
<div className={ styles.chart }> <div className={ styles.chart }>
<ResponsiveContainer <ResponsiveContainer
@ -370,11 +371,7 @@ export default class GasPriceSelector extends Component {
/> />
<Tooltip <Tooltip
wrapperStyle={ { wrapperStyle={ TOOL_STYLE }
backgroundColor: 'rgba(0, 0, 0, 0.75)',
padding: '0 0.5em',
fontSize: '0.9em'
} }
cursor={ this.renderCustomCursor() } cursor={ this.renderCustomCursor() }
content={ <CustomTooltip gasPriceHistogram={ gasPriceHistogram } /> } content={ <CustomTooltip gasPriceHistogram={ gasPriceHistogram } /> }
/> />
@ -394,7 +391,8 @@ export default class GasPriceSelector extends Component {
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
</div> </div>
</div>); </div>
);
} }
renderCustomCursor = () => { renderCustomCursor = () => {

View File

@ -14,11 +14,36 @@
/* 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/>.
*/ */
.signer {
.columns {
display: flex;
flex-wrap: wrap;
position: relative;
} }
.container { .graphColumn {
flex: 65;
} }
.mainContainer { .editColumn {
flex: 35;
padding-left: 1em;
justify-ontent: space-around;
padding-bottom: 12;
display: flex;
flex-wrap: wrap;
position: relative;
flex-direction: column;
}
.gasPriceDesc {
font-size: 0.75em;
opacity: 0.5;
}
.row {
display: flex;
flex-wrap: wrap;
position: relative;
flex-direction: column;
} }

View File

@ -0,0 +1,108 @@
// 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 BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import { observer } from 'mobx-react';
import Input from '../Form/Input';
import GasPriceSelector from './GasPriceSelector';
import Store from './store';
import styles from './gasPriceEditor.css';
@observer
export default class GasPriceEditor extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
store: PropTypes.object.isRequired,
onChange: PropTypes.func
}
static Store = Store;
render () {
const { api } = this.context;
const { store } = this.props;
const { estimated, priceDefault, price, gas, histogram, errorGas, errorPrice, errorTotal, totalValue } = store;
const eth = api.util.fromWei(totalValue).toFormat();
const gasLabel = `gas (estimated: ${new BigNumber(estimated).toFormat()})`;
const priceLabel = `price (current: ${new BigNumber(priceDefault).toFormat()})`;
return (
<div className={ styles.columns }>
<div className={ styles.graphColumn }>
<GasPriceSelector
gasPriceHistogram={ histogram }
gasPrice={ price }
onChange={ this.onEditGasPrice } />
<div className={ styles.gasPriceDesc }>
You can choose the gas price based on the
distribution of recent included transaction gas prices.
The lower the gas price is, the cheaper the transaction will
be. The higher the gas price is, the faster it should
get mined by the network.
</div>
</div>
<div className={ styles.editColumn }>
<div className={ styles.row }>
<Input
label={ gasLabel }
hint='the amount of gas to use for the transaction'
error={ errorGas }
value={ gas }
onChange={ this.onEditGas } />
<Input
label={ priceLabel }
hint='the price of gas to use for the transaction'
error={ errorPrice }
value={ price }
onChange={ this.onEditGasPrice } />
</div>
<div className={ styles.row }>
<Input
disabled
label='total transaction amount'
hint='the total amount of the transaction'
error={ errorTotal }
value={ `${eth} ETH` } />
</div>
</div>
</div>
);
}
onEditGas = (event, gas) => {
const { store, onChange } = this.props;
store.setGas(gas);
onChange && onChange('gas', gas);
}
onEditGasPrice = (event, price) => {
const { store, onChange } = this.props;
store.setPrice(price);
onChange && onChange('gasPrice', price);
}
}

View File

@ -14,6 +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 (chain) => { export default from './gasPriceEditor';
return chain === 'morden' || chain === 'ropsten' || chain === 'testnet';
};

View File

@ -0,0 +1,123 @@
// 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 BigNumber from 'bignumber.js';
import { action, computed, observable, transaction } from 'mobx';
import { ERRORS, validatePositiveNumber } from '~/util/validation';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants';
export default class GasPriceEditor {
@observable errorEstimated = null;
@observable errorGas = null;
@observable errorPrice = null;
@observable errorTotal = null;
@observable estimated = DEFAULT_GAS;
@observable histogram = null;
@observable price = DEFAULT_GASPRICE;
@observable priceDefault = DEFAULT_GASPRICE;
@observable gas = DEFAULT_GAS;
@observable gasLimit = 0;
@observable weiValue = '0';
constructor (api, gasLimit, loadDefaults = true) {
this._api = api;
this.gasLimit = gasLimit;
if (loadDefaults) {
this.loadDefaults();
}
}
@computed get totalValue () {
try {
return new BigNumber(this.gas).mul(this.price).add(this.weiValue);
} catch (error) {
return new BigNumber(0);
}
}
@action setErrorTotal = (errorTotal) => {
this.errorTotal = errorTotal;
}
@action setEstimated = (estimated) => {
transaction(() => {
const bn = new BigNumber(estimated);
this.estimated = estimated;
if (bn.gte(MAX_GAS_ESTIMATION)) {
this.errorEstimated = ERRORS.gasException;
} else if (bn.gte(this.gasLimit)) {
this.errorEstimated = ERRORS.gasBlockLimit;
} else {
this.errorEstimated = null;
}
});
}
@action setEthValue = (weiValue) => {
this.weiValue = weiValue;
}
@action setHistogram = (gasHistogram) => {
this.histogram = gasHistogram;
}
@action setPrice = (price) => {
transaction(() => {
this.errorPrice = validatePositiveNumber(price).numberError;
this.price = price;
});
}
@action setGas = (gas) => {
transaction(() => {
const { numberError } = validatePositiveNumber(gas);
const bn = new BigNumber(gas);
this.gas = gas;
if (numberError) {
this.errorGas = numberError;
} else if (bn.gte(this.gasLimit)) {
this.errorGas = ERRORS.gasBlockLimit;
} else {
this.errorGas = null;
}
});
}
@action loadDefaults () {
Promise
.all([
this._api.parity.gasPriceHistogram(),
this._api.eth.gasPrice()
])
.then(([gasPriceHistogram, gasPrice]) => {
transaction(() => {
this.setPrice(gasPrice.toFixed(0));
this.setHistogram(gasPriceHistogram);
this.priceDefault = gasPrice.toFixed();
});
})
.catch((error) => {
console.warn('getDefaults', error);
});
}
}

View File

@ -0,0 +1,76 @@
// 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';
import { mount } from 'enzyme';
import sinon from 'sinon';
import IdentityName from './identityName';
const ADDR_A = '0x123456789abcdef0123456789A';
const ADDR_B = '0x123456789abcdef0123456789B';
const ADDR_C = '0x123456789abcdef0123456789C';
const STORE = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
balances: {
tokens: {}
},
personal: {
accountsInfo: {
[ADDR_A]: { name: 'testing' },
[ADDR_B]: {}
}
}
};
}
};
function render (props) {
return mount(
<IdentityName
store={ STORE }
{ ...props } />
);
}
describe('ui/IdentityName', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
describe('account not found', () => {
it('renders null with empty', () => {
expect(render({ address: ADDR_C, empty: true }).html()).to.be.null;
});
it('renders address without empty', () => {
expect(render({ address: ADDR_C }).text()).to.equal(ADDR_C);
});
it('renders short address with shorten', () => {
expect(render({ address: ADDR_C, shorten: true }).text()).to.equal('123456…56789c');
});
it('renders unknown with flag', () => {
expect(render({ address: ADDR_C, unknown: true }).text()).to.equal('UNNAMED');
});
});
});
});

View File

@ -17,8 +17,8 @@
.layout { .layout {
padding: 0.25em 0.25em 1em 0.25em; padding: 0.25em 0.25em 1em 0.25em;
}
.layout>div { > * {
padding-bottom: 0.75em; margin-bottom: 0.75em;
}
} }

View File

@ -0,0 +1,55 @@
// 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 getMuiTheme from 'material-ui/styles/getMuiTheme';
import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme';
const muiTheme = getMuiTheme(lightBaseTheme);
import theme from './theme';
describe('ui/Theme', () => {
it('is MUI-based', () => {
expect(Object.keys(theme)).to.deep.equal(Object.keys(muiTheme).concat('parity'));
});
it('allows setting of Parity backgrounds', () => {
expect(typeof theme.parity.setBackgroundSeed === 'function').to.be.true;
expect(typeof theme.parity.getBackgroundStyle === 'function').to.be.true;
});
describe('parity', () => {
describe('setBackgroundSeed', () => {
const SEED = 'testseed';
beforeEach(() => {
theme.parity.setBackgroundSeed(SEED);
});
it('sets the correct theme values', () => {
expect(theme.parity.backgroundSeed).to.equal(SEED);
});
});
describe('getBackgroundStyle', () => {
it('generates a style containing background', () => {
const style = theme.parity.getBackgroundStyle();
expect(style).to.have.property('background');
});
});
});
});

View File

@ -20,7 +20,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { LinearProgress } from 'material-ui'; import { LinearProgress } from 'material-ui';
import { txLink } from '../../3rdparty/etherscan/links'; import { txLink } from '~/3rdparty/etherscan/links';
import ShortenedHash from '../ShortenedHash'; import ShortenedHash from '../ShortenedHash';
import styles from './txHash.css'; import styles from './txHash.css';

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 './Animated'; export default from './txRow';

View File

@ -0,0 +1,133 @@
// 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 moment from 'moment';
import React, { Component, PropTypes } from 'react';
import { txLink, addressLink } from '~/3rdparty/etherscan/links';
import IdentityIcon from '../../IdentityIcon';
import IdentityName from '../../IdentityName';
import MethodDecoding from '../../MethodDecoding';
import styles from '../txList.css';
export default class TxRow extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
tx: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
isTest: PropTypes.bool.isRequired,
block: PropTypes.object,
historic: PropTypes.bool,
className: PropTypes.string
};
static defaultProps = {
historic: true
};
render () {
const { tx, address, isTest, historic, className } = this.props;
return (
<tr className={ className || '' }>
{ this.renderBlockNumber(tx.blockNumber) }
{ this.renderAddress(tx.from) }
<td className={ styles.transaction }>
{ this.renderEtherValue(tx.value) }
<div></div>
<div>
<a
className={ styles.link }
href={ txLink(tx.hash, isTest) }
target='_blank'>
{ `${tx.hash.substr(2, 6)}...${tx.hash.slice(-6)}` }
</a>
</div>
</td>
{ this.renderAddress(tx.to) }
<td className={ styles.method }>
<MethodDecoding
historic={ historic }
address={ address }
transaction={ tx } />
</td>
</tr>
);
}
renderAddress (address) {
const { isTest } = this.props;
let esLink = null;
if (address) {
esLink = (
<a
href={ addressLink(address, isTest) }
target='_blank'
className={ styles.link }>
<IdentityName address={ address } shorten />
</a>
);
}
return (
<td className={ styles.address }>
<div className={ styles.center }>
<IdentityIcon
center
className={ styles.icon }
address={ address } />
</div>
<div className={ styles.center }>
{ esLink || 'DEPLOY' }
</div>
</td>
);
}
renderEtherValue (_value) {
const { api } = this.context;
const value = api.util.fromWei(_value);
if (value.eq(0)) {
return <div className={ styles.value }>{ ' ' }</div>;
}
return (
<div className={ styles.value }>
{ value.toFormat(5) }<small>ETH</small>
</div>
);
}
renderBlockNumber (_blockNumber) {
const { block } = this.props;
const blockNumber = _blockNumber.toNumber();
return (
<td className={ styles.timestamp }>
<div>{ blockNumber && block ? moment(block.timestamp).fromNow() : null }</div>
<div>{ blockNumber ? _blockNumber.toFormat() : 'Pending' }</div>
</td>
);
}
}

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 BigNumber from 'bignumber.js';
import React from 'react';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import Api from '~/api';
import TxRow from './txRow';
const api = new Api({ execute: sinon.stub() });
function renderShallow (props) {
return shallow(
<TxRow
{ ...props } />,
{ context: { api } }
);
}
describe('ui/TxRow', () => {
describe('rendering', () => {
it('renders defaults', () => {
const block = {
timestamp: new Date()
};
const tx = {
blockNumber: new BigNumber(123),
hash: '0x123456789abcdef0123456789abcdef0123456789abcdef',
value: new BigNumber(1)
};
expect(renderShallow({ block, tx })).to.be.ok;
});
});
});

View File

@ -45,6 +45,8 @@ export default class Store {
if (bnB.eq(0)) { if (bnB.eq(0)) {
return bnB.eq(bnA) ? 0 : 1; return bnB.eq(bnA) ? 0 : 1;
} else if (bnA.eq(0)) {
return -1;
} }
return bnB.comparedTo(bnA); return bnB.comparedTo(bnA);

View File

@ -0,0 +1,90 @@
// 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 BigNumber from 'bignumber.js';
import sinon from 'sinon';
import Store from './store';
const SUBID = 123;
const BLOCKS = {
1: { blockhash: '0x1' },
2: { blockhash: '0x2' }
};
const TRANSACTIONS = {
'0x123': { blockNumber: new BigNumber(1) },
'0x234': { blockNumber: new BigNumber(0) },
'0x345': { blockNumber: new BigNumber(2) },
'0x456': { blockNumber: new BigNumber(0) }
};
describe('ui/TxList/store', () => {
let api;
let store;
beforeEach(() => {
api = {
subscribe: sinon.stub().resolves(SUBID),
eth: {
getBlockByNumber: (blockNumber) => {
return Promise.resolve(BLOCKS[blockNumber]);
}
}
};
store = new Store(api);
});
describe('create', () => {
it('has empty storage', () => {
expect(store.blocks).to.deep.equal({});
expect(store.sortedHashes.peek()).to.deep.equal([]);
expect(store.transactions).to.deep.equal({});
});
it('subscribes to eth_blockNumber', () => {
expect(api.subscribe).to.have.been.calledWith('eth_blockNumber');
expect(store._subscriptionId).to.equal(SUBID);
});
});
describe('addBlocks', () => {
beforeEach(() => {
store.addBlocks(BLOCKS);
});
it('adds the blocks to the list', () => {
expect(store.blocks).to.deep.equal(BLOCKS);
});
});
describe('addTransactions', () => {
beforeEach(() => {
store.addTransactions(TRANSACTIONS);
});
it('adds all transactions to the list', () => {
expect(store.transactions).to.deep.equal(TRANSACTIONS);
});
it('sorts transactions based on blockNumber', () => {
expect(store.sortedHashes.peek()).to.deep.equal(['0x234', '0x456', '0x345', '0x123']);
});
it('adds pending transactions to the pending queue', () => {
expect(store._pendingHashes).to.deep.equal(['0x234', '0x456']);
});
});
});

View File

@ -14,128 +14,16 @@
// 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 moment from 'moment';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { txLink, addressLink } from '../../3rdparty/etherscan/links';
import IdentityIcon from '../IdentityIcon';
import IdentityName from '../IdentityName';
import MethodDecoding from '../MethodDecoding';
import Store from './store'; import Store from './store';
import TxRow from './TxRow';
import styles from './txList.css'; import styles from './txList.css';
export class TxRow extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
tx: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
isTest: PropTypes.bool.isRequired,
block: PropTypes.object,
historic: PropTypes.bool,
className: PropTypes.string
};
static defaultProps = {
historic: true
};
render () {
const { tx, address, isTest, historic, className } = this.props;
return (
<tr className={ className || '' }>
{ this.renderBlockNumber(tx.blockNumber) }
{ this.renderAddress(tx.from) }
<td className={ styles.transaction }>
{ this.renderEtherValue(tx.value) }
<div></div>
<div>
<a
className={ styles.link }
href={ txLink(tx.hash, isTest) }
target='_blank'>
{ `${tx.hash.substr(2, 6)}...${tx.hash.slice(-6)}` }
</a>
</div>
</td>
{ this.renderAddress(tx.to) }
<td className={ styles.method }>
<MethodDecoding
historic={ historic }
address={ address }
transaction={ tx } />
</td>
</tr>
);
}
renderAddress (address) {
const { isTest } = this.props;
let esLink = null;
if (address) {
esLink = (
<a
href={ addressLink(address, isTest) }
target='_blank'
className={ styles.link }>
<IdentityName address={ address } shorten />
</a>
);
}
return (
<td className={ styles.address }>
<div className={ styles.center }>
<IdentityIcon
center
className={ styles.icon }
address={ address } />
</div>
<div className={ styles.center }>
{ esLink || 'DEPLOY' }
</div>
</td>
);
}
renderEtherValue (_value) {
const { api } = this.context;
const value = api.util.fromWei(_value);
if (value.eq(0)) {
return <div className={ styles.value }>{ ' ' }</div>;
}
return (
<div className={ styles.value }>
{ value.toFormat(5) }<small>ETH</small>
</div>
);
}
renderBlockNumber (_blockNumber) {
const { block } = this.props;
const blockNumber = _blockNumber.toNumber();
return (
<td className={ styles.timestamp }>
<div>{ blockNumber && block ? moment(block.timestamp).fromNow() : null }</div>
<div>{ blockNumber ? _blockNumber.toFormat() : 'Pending' }</div>
</td>
);
}
}
@observer @observer
class TxList extends Component { class TxList extends Component {
static contextTypes = { static contextTypes = {

View File

@ -0,0 +1,54 @@
// 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';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import Api from '~/api';
import TxList from './txList';
const api = new Api({ execute: sinon.stub() });
const STORE = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
nodeStatus: {
isTest: true
}
};
}
};
function renderShallow (props) {
return shallow(
<TxList
store={ STORE }
{ ...props } />,
{ context: { api } }
);
}
describe('ui/TxList', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(renderShallow()).to.be.ok;
});
});
});

View File

@ -31,6 +31,7 @@ import CopyToClipboard from './CopyToClipboard';
import Editor from './Editor'; import Editor from './Editor';
import Errors from './Errors'; import Errors from './Errors';
import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form'; import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form';
import GasPriceEditor from './GasPriceEditor';
import IdentityIcon from './IdentityIcon'; import IdentityIcon from './IdentityIcon';
import IdentityName from './IdentityName'; import IdentityName from './IdentityName';
import Loading from './Loading'; import Loading from './Loading';
@ -67,7 +68,7 @@ export {
Errors, Errors,
Form, Form,
FormWrap, FormWrap,
TypedInput, GasPriceEditor,
Input, Input,
InputAddress, InputAddress,
InputAddressSelect, InputAddressSelect,
@ -91,5 +92,6 @@ export {
Tooltip, Tooltip,
Tooltips, Tooltips,
TxHash, TxHash,
TxList TxList,
TypedInput
}; };

View File

@ -18,7 +18,18 @@ const isValidReceipt = (receipt) => {
return receipt && receipt.blockNumber && receipt.blockNumber.gt(0); return receipt && receipt.blockNumber && receipt.blockNumber.gt(0);
}; };
const waitForConfirmations = (api, tx, confirmations) => { export function checkIfTxFailed (api, tx, gasSent) {
return api.pollMethod('eth_getTransactionReceipt', tx)
.then((receipt) => {
// TODO: Right now, there's no way to tell wether the EVM code crashed.
// Because you usually send a bit more gas than estimated (to make sure
// it gets mined quickly), we transaction probably failed if all the gas
// has been used up.
return receipt.gasUsed.eq(gasSent);
});
}
export function waitForConfirmations (api, tx, confirmations) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
api.pollMethod('eth_getTransactionReceipt', tx, isValidReceipt) api.pollMethod('eth_getTransactionReceipt', tx, isValidReceipt)
.then((receipt) => { .then((receipt) => {
@ -39,6 +50,4 @@ const waitForConfirmations = (api, tx, confirmations) => {
.catch(reject); .catch(reject);
}); });
}); });
}; }
export default waitForConfirmations;

View File

@ -20,6 +20,7 @@ import util from '~/api/util';
export const ERRORS = { export const ERRORS = {
invalidAddress: 'address is an invalid network address', invalidAddress: 'address is an invalid network address',
invalidAmount: 'the supplied amount should be a valid positive number',
duplicateAddress: 'the address is already in your address book', duplicateAddress: 'the address is already in your address book',
invalidChecksum: 'address has failed the checksum formatting', invalidChecksum: 'address has failed the checksum formatting',
invalidName: 'name should not be blank and longer than 2', invalidName: 'name should not be blank and longer than 2',
@ -27,7 +28,9 @@ export const ERRORS = {
invalidCode: 'code should be the compiled hex string', invalidCode: 'code should be the compiled hex string',
invalidNumber: 'invalid number format', invalidNumber: 'invalid number format',
negativeNumber: 'input number should be positive', negativeNumber: 'input number should be positive',
decimalNumber: 'input number should not contain decimals' decimalNumber: 'input number should not contain decimals',
gasException: 'the transaction will throw an exception with the current values',
gasBlockLimit: 'the transaction execution will exceed the block gas limit'
}; };
export function validateAbi (abi, api) { export function validateAbi (abi, api) {
@ -133,6 +136,25 @@ export function validateName (name) {
}; };
} }
export function validatePositiveNumber (number) {
let numberError = null;
try {
const v = new BigNumber(number);
if (v.lt(0)) {
numberError = ERRORS.invalidAmount;
}
} catch (e) {
numberError = ERRORS.invalidAmount;
}
return {
number,
numberError
};
}
export function validateUint (value) { export function validateUint (value) {
let valueError = null; let valueError = null;

View File

@ -14,9 +14,10 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { range } from 'lodash'; import { range, uniq } from 'lodash';
import { bytesToHex, toHex } from '~/api/util/format'; import { bytesToHex, toHex } from '~/api/util/format';
import { validateAddress } from '~/util/validation';
export default class WalletsUtils { export default class WalletsUtils {
@ -26,10 +27,82 @@ export default class WalletsUtils {
static fetchOwners (walletContract) { static fetchOwners (walletContract) {
const walletInstance = walletContract.instance; const walletInstance = walletContract.instance;
return walletInstance return walletInstance
.m_numOwners.call() .m_numOwners.call()
.then((mNumOwners) => { .then((mNumOwners) => {
return Promise.all(range(mNumOwners.toNumber()).map((idx) => walletInstance.getOwner.call({}, [ idx ]))); const promises = range(mNumOwners.toNumber())
.map((idx) => walletInstance.getOwner.call({}, [ idx ]));
return Promise
.all(promises)
.then((owners) => {
const uniqOwners = uniq(owners);
// If all owners are the zero account : must be Mist wallet contract
if (uniqOwners.length === 1 && /^(0x)?0*$/.test(owners[0])) {
return WalletsUtils.fetchMistOwners(walletContract, mNumOwners.toNumber());
}
return owners;
});
});
}
static fetchMistOwners (walletContract, mNumOwners) {
const walletAddress = walletContract.address;
return WalletsUtils
.getMistOwnersOffset(walletContract)
.then((result) => {
if (!result || result.offset === -1) {
return [];
}
const owners = [ result.address ];
if (mNumOwners === 1) {
return owners;
}
const initOffset = result.offset + 1;
let promise = Promise.resolve();
range(initOffset, initOffset + mNumOwners - 1).forEach((offset) => {
promise = promise
.then(() => {
return walletContract.api.eth.getStorageAt(walletAddress, offset);
})
.then((result) => {
const resultAddress = '0x' + (result || '').slice(-40);
const { address } = validateAddress(resultAddress);
owners.push(address);
});
});
return promise.then(() => owners);
});
}
static getMistOwnersOffset (walletContract, offset = 3) {
return walletContract.api.eth
.getStorageAt(walletContract.address, offset)
.then((result) => {
if (result && !/^(0x)?0*$/.test(result)) {
const resultAddress = '0x' + result.slice(-40);
const { address, addressError } = validateAddress(resultAddress);
if (!addressError) {
return { offset, address };
}
}
if (offset >= 100) {
return { offset: -1 };
}
return WalletsUtils.getMistOwnersOffset(walletContract, offset + 1);
}); });
} }

View File

@ -31,12 +31,14 @@ export default class Header extends Component {
account: PropTypes.object, account: PropTypes.object,
balance: PropTypes.object, balance: PropTypes.object,
className: PropTypes.string, className: PropTypes.string,
children: PropTypes.node children: PropTypes.node,
isContract: PropTypes.bool
}; };
static defaultProps = { static defaultProps = {
className: '', className: '',
children: null children: null,
isContract: false
}; };
render () { render () {
@ -88,9 +90,9 @@ export default class Header extends Component {
} }
renderTxCount () { renderTxCount () {
const { balance } = this.props; const { balance, isContract } = this.props;
if (!balance) { if (!balance || isContract) {
return null; return null;
} }

View File

@ -15,9 +15,6 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.transactions {
}
.infonone { .infonone {
opacity: 0.25; opacity: 0.25;
} }

View File

@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import etherscan from '../../../3rdparty/etherscan'; import etherscan from '~/3rdparty/etherscan';
import { Container, TxList } from '~/ui'; import { Container, TxList } from '~/ui';
import styles from './transactions.css'; import styles from './transactions.css';

View File

@ -14,8 +14,6 @@
/* 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/>.
*/ */
.account {
}
.btnicon { .btnicon {
width: 24px; width: 24px;

View File

@ -105,7 +105,7 @@ class Account extends Component {
} }
return ( return (
<div className={ styles.account }> <div>
{ this.renderDeleteDialog(account) } { this.renderDeleteDialog(account) }
{ this.renderEditDialog(account) } { this.renderEditDialog(account) }
{ this.renderFundDialog() } { this.renderFundDialog() }

View File

@ -14,6 +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 BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
@ -74,6 +75,13 @@ export default class Summary extends Component {
return true; return true;
} }
const prevOwners = this.props.owners;
const nextOwners = nextProps.owners;
if (!isEqual(prevOwners, nextOwners)) {
return true;
}
return false; return false;
} }
@ -113,16 +121,17 @@ export default class Summary extends Component {
renderOwners () { renderOwners () {
const { owners } = this.props; const { owners } = this.props;
const ownersValid = (owners || []).filter((owner) => owner.address && new BigNumber(owner.address).gt(0));
if (!owners || owners.length === 0) { if (!ownersValid || ownersValid.length === 0) {
return null; return null;
} }
return ( return (
<div className={ styles.owners }> <div className={ styles.owners }>
{ {
owners.map((owner) => ( ownersValid.map((owner, index) => (
<div key={ owner.address }> <div key={ `${index}_${owner.address}` }>
<div <div
data-tip data-tip
data-for={ `owner_${owner.address}` } data-for={ `owner_${owner.address}` }

View File

@ -14,8 +14,6 @@
/* 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/>.
*/ */
.accounts {
}
.accountTooltip { .accountTooltip {
top: 13.3em; top: 13.3em;

View File

@ -82,7 +82,7 @@ class Accounts extends Component {
render () { render () {
return ( return (
<div className={ styles.accounts }> <div>
{ this.renderNewDialog() } { this.renderNewDialog() }
{ this.renderNewWalletDialog() } { this.renderNewWalletDialog() }
{ this.renderActionbar() } { this.renderActionbar() }
@ -293,13 +293,17 @@ function mapStateToProps (state) {
const walletsOwners = Object const walletsOwners = Object
.keys(walletsInfo) .keys(walletsInfo)
.map((wallet) => ({ .map((wallet) => {
owners: walletsInfo[wallet].owners.map((owner) => ({ const owners = walletsInfo[wallet].owners || [];
return {
owners: owners.map((owner) => ({
address: owner, address: owner,
name: accountsInfo[owner] && accountsInfo[owner].name || owner name: accountsInfo[owner] && accountsInfo[owner].name || owner
})), })),
address: wallet address: wallet
})) };
})
.reduce((walletsOwners, wallet) => { .reduce((walletsOwners, wallet) => {
walletsOwners[wallet.address] = wallet.owners; walletsOwners[wallet.address] = wallet.owners;
return walletsOwners; return walletsOwners;

View File

@ -14,37 +14,37 @@
/* 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/>.
*/ */
.address {
}
.delete .hero { .delete {
.hero {
padding-bottom: 1em; padding-bottom: 1em;
} }
.delete .info { .info {
display: inline-block; display: inline-block;
} }
.delete .icon { .icon {
display: inline-block; display: inline-block;
} }
.delete .nameinfo { .nameinfo {
display: inline-block; display: inline-block;
text-align: left; text-align: left;
} }
.delete .header { .header {
text-transform: uppercase; text-transform: uppercase;
font-size: 1.25em; font-size: 1.25em;
padding-bottom: 0.25em; padding-bottom: 0.25em;
} }
.delete .address { .address {
} }
.delete .description { .description {
padding-top: 1em; padding-top: 1em;
font-size: 0.75em; font-size: 0.75em;
color: #aaa; color: #aaa;
} }
}

View File

@ -28,8 +28,6 @@ import Transactions from '../Account/Transactions';
import Delete from './Delete'; import Delete from './Delete';
import { setVisibleAccounts } from '~/redux/providers/personalActions'; import { setVisibleAccounts } from '~/redux/providers/personalActions';
import styles from './address.css';
class Address extends Component { class Address extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired, api: PropTypes.object.isRequired,
@ -85,7 +83,7 @@ class Address extends Component {
} }
return ( return (
<div className={ styles.address }> <div>
{ this.renderEditDialog(contact) } { this.renderEditDialog(contact) }
{ this.renderActionbar(contact) } { this.renderActionbar(contact) }
<Delete <Delete

View File

@ -14,8 +14,6 @@
/* 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/>.
*/ */
.addresses {
}
.list { .list {
display: flex; display: flex;
@ -26,21 +24,21 @@
flex: 0 1 50%; flex: 0 1 50%;
width: 50%; width: 50%;
position: relative; position: relative;
}
.address:nth-child(odd)>div { &:nth-child(odd)>div {
padding-right: 0.5em !important; padding-right: 0.5em !important;
} }
.address:nth-child(even)>div { &:nth-child(even)>div {
padding-left: 0.5em !important; padding-left: 0.5em !important;
} }
}
.empty { .empty {
width: 100%; width: 100%;
display: block; display: block;
}
.empty div { div {
color: #aaa; color: #aaa;
} }
}

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