diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6df465035..84d10e1bc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -422,10 +422,10 @@ test-rust-stable: image: ethcore/rust:stable before_script: - git submodule update --init --recursive - - export RUST_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep -v ^js/ | wc -l) + - export RUST_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep -v -e ^js -e ^\\. -e ^LICENSE -e ^README.md -e ^appveyor.yml -e ^test.sh -e ^windows/ -e ^scripts/ -e^mac/ -e ^nsis/ | wc -l) script: - export RUST_BACKTRACE=1 - - if [ "$RUST_FILES_MODIFIED" = 0 ]; then echo "Skipping Rust tests since no Rust files modified."; else ./test.sh $CARGOFLAGS; fi + - if [ $RUST_FILES_MODIFIED -eq 0 ]; then echo "Skipping Rust tests since no Rust files modified."; else ./test.sh $CARGOFLAGS; fi tags: - rust - rust-stable @@ -435,9 +435,9 @@ js-test: before_script: - 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) - - if [ "$JS_FILES_MODIFIED" = 0 ]; then echo "Skipping JS deps install since no JS files modified."; else ./js/scripts/install-deps.sh;fi + - if [ $JS_FILES_MODIFIED -eq 0 ]; then echo "Skipping JS deps install since no JS files modified."; else ./js/scripts/install-deps.sh;fi script: - - 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 -eq 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: - rust - rust-stable @@ -480,9 +480,9 @@ js-release: before_script: - export JS_FILES_MODIFIED=$(git --no-pager diff --name-only $CI_BUILD_REF^ $CI_BUILD_REF | grep ^js/ | wc -l) - echo $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 + - if [ $JS_FILES_MODIFIED -eq 0 ]; then echo "Skipping JS deps install since no JS files modified."; else ./js/scripts/install-deps.sh;fi script: - echo $JS_FILES_MODIFIED - - 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 + - if [ $JS_FILES_MODIFIED -eq 0 ]; then echo "Skipping JS rebuild since no JS files modified."; else ./js/scripts/build.sh && ./js/scripts/release.sh; fi tags: - javascript diff --git a/Cargo.lock b/Cargo.lock index 7e970051c..fcf12f1f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1290,7 +1290,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#3d3b2f9e8e8b0fd62c172240bfd001a317cf2979" +source = "git+https://github.com/ethcore/js-precompiled.git#e1c4592ba60727af643c54941bf97f63cd9b5737" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/ethcore/light/src/client.rs b/ethcore/light/src/client.rs index 0035406dc..fcfff81e6 100644 --- a/ethcore/light/src/client.rs +++ b/ethcore/light/src/client.rs @@ -14,8 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -//! Light client implementation. Used for raw data queries as well as the header -//! sync. +//! Light client implementation. Stores data from light sync use std::sync::Arc; @@ -29,7 +28,7 @@ use ethcore::transaction::SignedTransaction; use ethcore::blockchain_info::BlockChainInfo; use io::IoChannel; -use util::hash::H256; +use util::hash::{H256, H256FastMap}; use util::{Bytes, Mutex}; use provider::Provider; @@ -37,9 +36,10 @@ use request; /// Light client implementation. pub struct Client { - engine: Arc, + _engine: Arc, header_queue: HeaderQueue, - message_channel: Mutex>, + _message_channel: Mutex>, + tx_pool: Mutex>, } impl Client { @@ -55,12 +55,17 @@ impl Client { false } + /// Import a local transaction. + pub fn import_own_transaction(&self, tx: SignedTransaction) { + self.tx_pool.lock().insert(tx.hash(), tx); + } + /// Fetch a vector of all pending transactions. pub fn pending_transactions(&self) -> Vec { - vec![] + self.tx_pool.lock().values().cloned().collect() } - /// Inquire about the status of a given block. + /// Inquire about the status of a given block (or header). pub fn status(&self, _id: BlockId) -> BlockStatus { BlockStatus::Unknown } diff --git a/ethcore/light/src/lib.rs b/ethcore/light/src/lib.rs index 7fa2f5911..f9008ac7c 100644 --- a/ethcore/light/src/lib.rs +++ b/ethcore/light/src/lib.rs @@ -28,8 +28,7 @@ //! It starts by performing a header-only sync, verifying random samples //! of members of the chain to varying degrees. -// TODO: remove when integrating with the rest of parity. -#![allow(dead_code)] +#![deny(missing_docs)] pub mod client; pub mod net; diff --git a/ethcore/light/src/net/context.rs b/ethcore/light/src/net/context.rs index 522722bcd..bd6d9e78b 100644 --- a/ethcore/light/src/net/context.rs +++ b/ethcore/light/src/net/context.rs @@ -26,112 +26,113 @@ use request::Request; /// 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); + /// Send a packet to a specific peer. + fn send(&self, peer: PeerId, packet_id: u8, packet_body: Vec); - /// 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); + /// 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); - /// Disconnect a peer. - fn disconnect_peer(&self, peer: PeerId); + /// 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); + /// 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; + /// Get a peer's protocol version. + fn protocol_version(&self, peer: PeerId) -> Option; /// Persistent peer id - fn persistent_peer_id(&self, peer: &PeerId) -> Option; + fn persistent_peer_id(&self, peer: PeerId) -> Option; } + impl<'a> IoContext for NetworkContext<'a> { - fn send(&self, peer: PeerId, packet_id: u8, packet_body: Vec) { - if let Err(e) = self.send(peer, packet_id, packet_body) { - debug!(target: "les", "Error sending packet to peer {}: {}", peer, e); - } - } + fn send(&self, peer: PeerId, packet_id: u8, packet_body: Vec) { + 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) { - if let Err(e) = self.respond(packet_id, packet_body) { - debug!(target: "les", "Error responding to peer message: {}", e); - } - } + fn respond(&self, packet_id: u8, packet_body: Vec) { + 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 disconnect_peer(&self, peer: PeerId) { + NetworkContext::disconnect_peer(self, peer); + } - fn disable_peer(&self, peer: PeerId) { - NetworkContext::disable_peer(self, peer); - } + fn disable_peer(&self, peer: PeerId) { + NetworkContext::disable_peer(self, peer); + } - fn protocol_version(&self, peer: PeerId) -> Option { - self.protocol_version(self.subprotocol_name(), peer) - } + fn protocol_version(&self, peer: PeerId) -> Option { + self.protocol_version(self.subprotocol_name(), peer) + } - fn persistent_peer_id(&self, peer: &PeerId) -> Option { - self.session_info(*peer).and_then(|info| info.id) + fn persistent_peer_id(&self, peer: PeerId) -> Option { + self.session_info(peer).and_then(|info| info.id) } } /// 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; + /// Get the peer relevant to the event e.g. message sender, + /// disconnected/connected peer. + fn peer(&self) -> PeerId; /// Returns the relevant's peer persistent Id (aka NodeId). - fn persistent_peer_id(&self, id: &PeerId) -> Option; + fn persistent_peer_id(&self, peer: PeerId) -> Option; - /// Make a request from a peer. - fn request_from(&self, peer: PeerId, request: Request) -> Result; + /// Make a request from a peer. + fn request_from(&self, peer: PeerId, request: Request) -> Result; - /// 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); + /// 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); + /// Disconnect a peer. + fn disconnect_peer(&self, peer: PeerId); - /// Disable a peer. - fn disable_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, + /// 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 { + fn peer(&self) -> PeerId { self.peer } - fn persistent_peer_id(&self, id: &PeerId) -> Option { + fn persistent_peer_id(&self, id: PeerId) -> Option { self.io.persistent_peer_id(id) } - fn request_from(&self, peer: PeerId, request: Request) -> Result { - self.proto.request_from(self.io, &peer, request) - } - fn make_announcement(&self, announcement: Announcement) { - self.proto.make_announcement(self.io, announcement); - } + fn request_from(&self, peer: PeerId, request: Request) -> Result { + self.proto.request_from(self.io, &peer, request) + } - fn disconnect_peer(&self, peer: PeerId) { - self.io.disconnect_peer(peer); - } + fn make_announcement(&self, announcement: Announcement) { + self.proto.make_announcement(self.io, announcement); + } - fn disable_peer(&self, peer: PeerId) { - self.io.disable_peer(peer); - } + fn disconnect_peer(&self, peer: PeerId) { + self.io.disconnect_peer(peer); + } + + fn disable_peer(&self, peer: PeerId) { + self.io.disable_peer(peer); + } } diff --git a/ethcore/light/src/net/mod.rs b/ethcore/light/src/net/mod.rs index 481740a48..e5bf0cb2b 100644 --- a/ethcore/light/src/net/mod.rs +++ b/ethcore/light/src/net/mod.rs @@ -34,7 +34,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use provider::Provider; -use request::{self, Request}; +use request::{self, HashOrNumber, Request}; use self::buffer_flow::{Buffer, FlowParams}; use self::context::Ctx; @@ -57,13 +57,13 @@ const TIMEOUT_INTERVAL_MS: u64 = 1000; // minimum interval between updates. const UPDATE_INTERVAL_MS: i64 = 5000; -// Supported protocol versions. +/// Supported protocol versions. pub const PROTOCOL_VERSIONS: &'static [u8] = &[1]; -// Max protocol version. +/// Max protocol version. pub const MAX_PROTOCOL_VERSION: u8 = 1; -// Packet count for LES. +/// Packet count for LES. pub const PACKET_COUNT: u8 = 15; // packet ID definitions. @@ -102,6 +102,18 @@ mod packet { pub const HEADER_PROOFS: u8 = 0x0e; } +// timeouts for different kinds of requests. all values are in milliseconds. +// TODO: variable timeouts based on request count. +mod timeout { + pub const HANDSHAKE: i64 = 2500; + pub const HEADERS: i64 = 5000; + pub const BODIES: i64 = 5000; + pub const RECEIPTS: i64 = 3500; + pub const PROOFS: i64 = 4000; + pub const CONTRACT_CODES: i64 = 5000; + pub const HEADER_PROOFS: i64 = 3500; +} + /// A request id. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ReqId(usize); @@ -111,7 +123,6 @@ pub struct ReqId(usize); struct PendingPeer { sent_head: H256, last_update: SteadyTime, - proto_version: u8, } // data about each peer. @@ -122,7 +133,6 @@ struct Peer { remote_flow: Option<(Buffer, FlowParams)>, sent_head: H256, // last head we've given them. last_update: SteadyTime, - proto_version: u8, } impl Peer { @@ -443,17 +453,54 @@ impl LightProtocol { } }; - // 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) + punish(*peer, io, e); + } + } + + // check timeouts and punish peers. + fn timeout_check(&self, io: &IoContext) { + let now = SteadyTime::now(); + + // handshake timeout + { + let mut pending = self.pending_peers.write(); + let slowpokes: Vec<_> = pending.iter() + .filter(|&(_, ref peer)| { + peer.last_update + Duration::milliseconds(timeout::HANDSHAKE) <= now + }) + .map(|(&p, _)| p) + .collect(); + + for slowpoke in slowpokes { + debug!(target: "les", "Peer {} handshake timed out", slowpoke); + pending.remove(&slowpoke); + io.disconnect_peer(slowpoke); + } + } + + // request timeouts + { + for r in self.pending_requests.read().values() { + let kind_timeout = match r.request.kind() { + request::Kind::Headers => timeout::HEADERS, + request::Kind::Bodies => timeout::BODIES, + request::Kind::Receipts => timeout::RECEIPTS, + request::Kind::StateProofs => timeout::PROOFS, + request::Kind::Codes => timeout::CONTRACT_CODES, + request::Kind::HeaderProofs => timeout::HEADER_PROOFS, + }; + + if r.timestamp + Duration::milliseconds(kind_timeout) <= now { + debug!(target: "les", "Request for {:?} from peer {} timed out", + r.request.kind(), r.peer_id); + + // keep the request in the `pending` set for now so + // on_disconnect will pass unfulfilled ReqIds to handlers. + // in the case that a response is received after this, the + // disconnect won't be cancelled but the ReqId won't be + // marked as abandoned. + io.disconnect_peer(r.peer_id); } } } @@ -463,19 +510,37 @@ impl LightProtocol { impl LightProtocol { // called when a peer connects. fn on_connect(&self, peer: &PeerId, io: &IoContext) { - let peer = *peer; + let proto_version = match io.protocol_version(*peer).ok_or(Error::WrongNetwork) { + Ok(pv) => pv, + Err(e) => { punish(*peer, io, e); return } + }; - trace!(target: "les", "Peer {} connecting", peer); - - match self.send_status(peer, io) { - Ok(pending_peer) => { - self.pending_peers.write().insert(peer, pending_peer); - } - Err(e) => { - trace!(target: "les", "Error while sending status: {}", e); - io.disconnect_peer(peer); - } + if PROTOCOL_VERSIONS.iter().find(|x| **x == proto_version).is_none() { + punish(*peer, io, Error::UnsupportedProtocolVersion(proto_version)); + return; } + + let chain_info = self.provider.chain_info(); + + let status = Status { + 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, + protocol_version: proto_version as u32, // match peer proto version + network_id: self.network_id, + last_head: None, + }; + + let capabilities = self.capabilities.read().clone(); + let status_packet = status::write_handshake(&status, &capabilities, Some(&self.flow_params)); + + self.pending_peers.write().insert(*peer, PendingPeer { + sent_head: chain_info.best_block_hash, + last_update: SteadyTime::now(), + }); + + io.send(*peer, packet::STATUS, status_packet); } // called when a peer disconnects. @@ -508,38 +573,6 @@ impl LightProtocol { } } - // send status to a peer. - fn send_status(&self, peer: PeerId, io: &IoContext) -> Result { - let proto_version = try!(io.protocol_version(peer).ok_or(Error::WrongNetwork)); - - 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 { - 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, - protocol_version: proto_version as u32, // match peer proto version - network_id: self.network_id, - last_head: None, - }; - - let capabilities = self.capabilities.read().clone(); - let status_packet = status::write_handshake(&status, &capabilities, Some(&self.flow_params)); - - io.send(peer, packet::STATUS, status_packet); - - Ok(PendingPeer { - sent_head: chain_info.best_block_hash, - last_update: SteadyTime::now(), - proto_version: proto_version, - }) - } - // Handle status message from peer. fn status(&self, peer: &PeerId, io: &IoContext, data: UntrustedRlp) -> Result<(), Error> { let pending = match self.pending_peers.write().remove(peer) { @@ -570,7 +603,6 @@ impl LightProtocol { remote_flow: remote_flow, sent_head: pending.sent_head, last_update: pending.last_update, - proto_version: pending.proto_version, })); for handler in &self.handlers { @@ -630,7 +662,7 @@ impl LightProtocol { } // Handle a request for block headers. - fn get_block_headers(&self, peer: &PeerId, io: &IoContext, data: UntrustedRlp) -> Result<(), Error> { + fn get_block_headers(&self, peer: &PeerId, io: &IoContext, data: UntrustedRlp) -> Result<(), Error> { const MAX_HEADERS: usize = 512; let peers = self.peers.read(); @@ -645,18 +677,21 @@ impl LightProtocol { let mut peer = peer.lock(); let req_id: u64 = try!(data.val_at(0)); + let data = try!(data.at(1)); - let block = { - let rlp = try!(data.at(1)); - (try!(rlp.val_at(0)), try!(rlp.val_at(1))) + let start_block = { + if try!(data.at(0)).size() == 32 { + HashOrNumber::Hash(try!(data.val_at(0))) + } else { + HashOrNumber::Number(try!(data.val_at(0))) + } }; let req = request::Headers { - block_num: block.0, - block_hash: block.1, - max: ::std::cmp::min(MAX_HEADERS, try!(data.val_at(2))), - skip: try!(data.val_at(3)), - reverse: try!(data.val_at(4)), + start: start_block, + max: ::std::cmp::min(MAX_HEADERS, try!(data.val_at(1))), + skip: try!(data.val_at(2)), + reverse: try!(data.val_at(3)), }; let max_cost = try!(peer.deduct_max(&self.flow_params, request::Kind::Headers, req.max)); @@ -667,8 +702,8 @@ impl LightProtocol { let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost); io.respond(packet::BLOCK_HEADERS, { - let mut stream = RlpStream::new_list(response.len() + 2); - stream.append(&req_id).append(&cur_buffer); + let mut stream = RlpStream::new_list(3); + stream.append(&req_id).append(&cur_buffer).begin_list(response.len()); for header in response { stream.append_raw(&header, 1); @@ -683,7 +718,7 @@ impl LightProtocol { // Receive a response for block headers. fn block_headers(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> { 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(); + let raw_headers: Vec<_> = try!(raw.at(2)).iter().map(|x| x.as_raw().to_owned()).collect(); for handler in &self.handlers { handler.on_block_headers(&Ctx { @@ -713,7 +748,7 @@ impl LightProtocol { let req_id: u64 = try!(data.val_at(0)); let req = request::Bodies { - block_hashes: try!(data.iter().skip(1).take(MAX_BODIES).map(|x| x.as_val()).collect()) + block_hashes: try!(try!(data.at(1)).iter().take(MAX_BODIES).map(|x| x.as_val()).collect()) }; let max_cost = try!(peer.deduct_max(&self.flow_params, request::Kind::Bodies, req.block_hashes.len())); @@ -726,8 +761,8 @@ impl LightProtocol { let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost); io.respond(packet::BLOCK_BODIES, { - let mut stream = RlpStream::new_list(response.len() + 2); - stream.append(&req_id).append(&cur_buffer); + let mut stream = RlpStream::new_list(3); + stream.append(&req_id).append(&cur_buffer).begin_list(response.len()); for body in response { stream.append_raw(&body, 1); @@ -742,7 +777,7 @@ impl LightProtocol { // Receive a response for block bodies. fn block_bodies(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> { let req_id = try!(self.pre_verify_response(peer, request::Kind::Bodies, &raw)); - let raw_bodies: Vec = raw.iter().skip(2).map(|x| x.as_raw().to_owned()).collect(); + let raw_bodies: Vec = try!(raw.at(2)).iter().map(|x| x.as_raw().to_owned()).collect(); for handler in &self.handlers { handler.on_block_bodies(&Ctx { @@ -772,7 +807,7 @@ impl LightProtocol { let req_id: u64 = try!(data.val_at(0)); let req = request::Receipts { - block_hashes: try!(data.iter().skip(1).take(MAX_RECEIPTS).map(|x| x.as_val()).collect()) + block_hashes: try!(try!(data.at(1)).iter().take(MAX_RECEIPTS).map(|x| x.as_val()).collect()) }; let max_cost = try!(peer.deduct_max(&self.flow_params, request::Kind::Receipts, req.block_hashes.len())); @@ -785,8 +820,8 @@ impl LightProtocol { let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost); io.respond(packet::RECEIPTS, { - let mut stream = RlpStream::new_list(response.len() + 2); - stream.append(&req_id).append(&cur_buffer); + let mut stream = RlpStream::new_list(3); + stream.append(&req_id).append(&cur_buffer).begin_list(response.len()); for receipts in response { stream.append_raw(&receipts, 1); @@ -801,9 +836,8 @@ impl LightProtocol { // Receive a response for receipts. fn receipts(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> { let req_id = try!(self.pre_verify_response(peer, request::Kind::Receipts, &raw)); - let raw_receipts: Vec> = try!(raw + let raw_receipts: Vec> = try!(try!(raw.at(2)) .iter() - .skip(2) .map(|x| x.as_val()) .collect()); @@ -835,7 +869,7 @@ impl LightProtocol { let req_id: u64 = try!(data.val_at(0)); let req = { - let requests: Result, Error> = data.iter().skip(1).take(MAX_PROOFS).map(|x| { + let requests: Result, Error> = try!(data.at(1)).iter().take(MAX_PROOFS).map(|x| { Ok(request::StateProof { block: try!(x.val_at(0)), key1: try!(x.val_at(1)), @@ -859,8 +893,8 @@ impl LightProtocol { let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost); io.respond(packet::PROOFS, { - let mut stream = RlpStream::new_list(response.len() + 2); - stream.append(&req_id).append(&cur_buffer); + let mut stream = RlpStream::new_list(3); + stream.append(&req_id).append(&cur_buffer).begin_list(response.len()); for proof in response { stream.append_raw(&proof, 1); @@ -876,8 +910,7 @@ impl LightProtocol { fn proofs(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> { let req_id = try!(self.pre_verify_response(peer, request::Kind::StateProofs, &raw)); - let raw_proofs: Vec> = raw.iter() - .skip(2) + let raw_proofs: Vec> = try!(raw.at(2)).iter() .map(|x| x.iter().map(|node| node.as_raw().to_owned()).collect()) .collect(); @@ -909,7 +942,7 @@ impl LightProtocol { let req_id: u64 = try!(data.val_at(0)); let req = { - let requests: Result, Error> = data.iter().skip(1).take(MAX_CODES).map(|x| { + let requests: Result, Error> = try!(data.at(1)).iter().take(MAX_CODES).map(|x| { Ok(request::ContractCode { block_hash: try!(x.val_at(0)), account_key: try!(x.val_at(1)), @@ -931,8 +964,8 @@ impl LightProtocol { let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost); io.respond(packet::CONTRACT_CODES, { - let mut stream = RlpStream::new_list(response.len() + 2); - stream.append(&req_id).append(&cur_buffer); + let mut stream = RlpStream::new_list(3); + stream.append(&req_id).append(&cur_buffer).begin_list(response.len()); for code in response { stream.append(&code); @@ -948,7 +981,7 @@ impl LightProtocol { fn contract_code(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> { let req_id = try!(self.pre_verify_response(peer, request::Kind::Codes, &raw)); - let raw_code: Vec = try!(raw.iter().skip(2).map(|x| x.as_val()).collect()); + let raw_code: Vec = try!(try!(raw.at(2)).iter().map(|x| x.as_val()).collect()); for handler in &self.handlers { handler.on_code(&Ctx { @@ -978,7 +1011,7 @@ impl LightProtocol { let req_id: u64 = try!(data.val_at(0)); let req = { - let requests: Result, Error> = data.iter().skip(1).take(MAX_PROOFS).map(|x| { + let requests: Result, Error> = try!(data.at(1)).iter().take(MAX_PROOFS).map(|x| { Ok(request::HeaderProof { cht_number: try!(x.val_at(0)), block_number: try!(x.val_at(1)), @@ -1001,8 +1034,8 @@ impl LightProtocol { let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost); io.respond(packet::HEADER_PROOFS, { - let mut stream = RlpStream::new_list(response.len() + 2); - stream.append(&req_id).append(&cur_buffer); + let mut stream = RlpStream::new_list(3); + stream.append(&req_id).append(&cur_buffer).begin_list(response.len()); for proof in response { stream.append_raw(&proof, 1); @@ -1023,9 +1056,8 @@ impl LightProtocol { )) } - 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()); + let raw_proofs: Vec<_> = try!(try!(raw.at(2)).iter().map(decode_res).collect()); for handler in &self.handlers { handler.on_header_proofs(&Ctx { @@ -1058,6 +1090,21 @@ impl LightProtocol { } } +// if something went wrong, figure out how much to punish the peer. +fn punish(peer: PeerId, io: &IoContext, e: Error) { + 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 NetworkProtocolHandler for LightProtocol { fn initialize(&self, io: &NetworkContext) { io.register_timer(TIMEOUT, TIMEOUT_INTERVAL_MS).expect("Error registering sync timer."); @@ -1075,11 +1122,9 @@ impl NetworkProtocolHandler for LightProtocol { self.on_disconnect(*peer, io); } - fn timeout(&self, _io: &NetworkContext, timer: TimerToken) { + fn timeout(&self, io: &NetworkContext, timer: TimerToken) { match timer { - TIMEOUT => { - // broadcast transactions to peers. - } + TIMEOUT => self.timeout_check(io), _ => warn!(target: "les", "received timeout on unknown token {}", timer), } } @@ -1089,20 +1134,24 @@ impl NetworkProtocolHandler for LightProtocol { fn encode_request(req: &Request, req_id: usize) -> Vec { match *req { Request::Headers(ref headers) => { - let mut stream = RlpStream::new_list(5); + let mut stream = RlpStream::new_list(2); + stream.append(&req_id).begin_list(4); + + match headers.start { + HashOrNumber::Hash(ref hash) => stream.append(hash), + HashOrNumber::Number(ref num) => stream.append(num), + }; + stream - .append(&req_id) - .begin_list(2) - .append(&headers.block_num) - .append(&headers.block_hash) .append(&headers.max) .append(&headers.skip) .append(&headers.reverse); + stream.out() } Request::Bodies(ref request) => { - let mut stream = RlpStream::new_list(request.block_hashes.len() + 1); - stream.append(&req_id); + let mut stream = RlpStream::new_list(2); + stream.append(&req_id).begin_list(request.block_hashes.len()); for hash in &request.block_hashes { stream.append(hash); @@ -1111,8 +1160,8 @@ fn encode_request(req: &Request, req_id: usize) -> Vec { stream.out() } Request::Receipts(ref request) => { - let mut stream = RlpStream::new_list(request.block_hashes.len() + 1); - stream.append(&req_id); + let mut stream = RlpStream::new_list(2); + stream.append(&req_id).begin_list(request.block_hashes.len()); for hash in &request.block_hashes { stream.append(hash); @@ -1121,8 +1170,8 @@ fn encode_request(req: &Request, req_id: usize) -> Vec { stream.out() } Request::StateProofs(ref request) => { - let mut stream = RlpStream::new_list(request.requests.len() + 1); - stream.append(&req_id); + let mut stream = RlpStream::new_list(2); + stream.append(&req_id).begin_list(request.requests.len()); for proof_req in &request.requests { stream.begin_list(4) @@ -1140,8 +1189,8 @@ fn encode_request(req: &Request, req_id: usize) -> Vec { stream.out() } Request::Codes(ref request) => { - let mut stream = RlpStream::new_list(request.code_requests.len() + 1); - stream.append(&req_id); + let mut stream = RlpStream::new_list(2); + stream.append(&req_id).begin_list(request.code_requests.len()); for code_req in &request.code_requests { stream.begin_list(2) @@ -1152,8 +1201,8 @@ fn encode_request(req: &Request, req_id: usize) -> Vec { stream.out() } Request::HeaderProofs(ref request) => { - let mut stream = RlpStream::new_list(request.requests.len() + 1); - stream.append(&req_id); + let mut stream = RlpStream::new_list(2); + stream.append(&req_id).begin_list(request.requests.len()); for proof_req in &request.requests { stream.begin_list(3) diff --git a/ethcore/light/src/net/tests/mod.rs b/ethcore/light/src/net/tests/mod.rs index 227eb9b7c..0b3226f0d 100644 --- a/ethcore/light/src/net/tests/mod.rs +++ b/ethcore/light/src/net/tests/mod.rs @@ -95,16 +95,27 @@ impl Provider for TestProvider { } fn block_headers(&self, req: request::Headers) -> Vec { - let best_num = self.0.client.chain_info().best_block_number; - let start_num = req.block_num; + use request::HashOrNumber; + use ethcore::views::HeaderView; - 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![] + let best_num = self.chain_info().best_block_number; + let start_num = match req.start { + HashOrNumber::Number(start_num) => start_num, + HashOrNumber::Hash(hash) => match self.0.client.block_header(BlockId::Hash(hash)) { + None => { + return Vec::new(); + } + Some(header) => { + let num = HeaderView::new(&header).number(); + if req.max == 1 || self.0.client.block_hash(BlockId::Number(num)) != Some(hash) { + // Non-canonical header or single header requested. + return vec![header]; + } + + num + } } - } + }; (0u64..req.max as u64) .map(|x: u64| x.saturating_mul(req.skip + 1)) @@ -254,8 +265,7 @@ fn buffer_overflow() { // 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, + start: 1.into(), max: 1000, skip: 0, reverse: false, @@ -288,8 +298,7 @@ fn get_block_headers() { } let request = Headers { - block_num: 1, - block_hash: provider.client.block_hash(BlockId::Number(1)).unwrap(), + start: 1.into(), max: 10, skip: 0, reverse: false, @@ -303,9 +312,9 @@ fn get_block_headers() { let new_buf = *flow_params.limit() - flow_params.compute_cost(request::Kind::Headers, 10); - let mut response_stream = RlpStream::new_list(12); + let mut response_stream = RlpStream::new_list(3); - response_stream.append(&req_id).append(&new_buf); + response_stream.append(&req_id).append(&new_buf).begin_list(10); for header in headers { response_stream.append_raw(&header, 1); } @@ -350,9 +359,9 @@ fn get_block_bodies() { let new_buf = *flow_params.limit() - flow_params.compute_cost(request::Kind::Bodies, 10); - let mut response_stream = RlpStream::new_list(12); + let mut response_stream = RlpStream::new_list(3); - response_stream.append(&req_id).append(&new_buf); + response_stream.append(&req_id).append(&new_buf).begin_list(10); for body in bodies { response_stream.append_raw(&body, 1); } @@ -403,9 +412,9 @@ fn get_block_receipts() { 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()); + let mut response_stream = RlpStream::new_list(3); - response_stream.append(&req_id).append(&new_buf); + response_stream.append(&req_id).append(&new_buf).begin_list(receipts.len()); for block_receipts in receipts { response_stream.append_raw(&block_receipts, 1); } @@ -452,9 +461,9 @@ fn get_state_proofs() { let new_buf = *flow_params.limit() - flow_params.compute_cost(request::Kind::StateProofs, 2); - let mut response_stream = RlpStream::new_list(4); + let mut response_stream = RlpStream::new_list(3); - response_stream.append(&req_id).append(&new_buf); + response_stream.append(&req_id).append(&new_buf).begin_list(2); for proof in proofs { response_stream.append_raw(&proof, 1); } @@ -501,9 +510,9 @@ fn get_contract_code() { let new_buf = *flow_params.limit() - flow_params.compute_cost(request::Kind::Codes, 2); - let mut response_stream = RlpStream::new_list(4); + let mut response_stream = RlpStream::new_list(3); - response_stream.append(&req_id).append(&new_buf); + response_stream.append(&req_id).append(&new_buf).begin_list(2); for code in codes { response_stream.append(&code); } diff --git a/ethcore/light/src/provider.rs b/ethcore/light/src/provider.rs index ad8d8ea16..ed2f49f5d 100644 --- a/ethcore/light/src/provider.rs +++ b/ethcore/light/src/provider.rs @@ -97,17 +97,29 @@ impl Provider for T { } fn block_headers(&self, req: request::Headers) -> Vec { + use request::HashOrNumber; + use ethcore::views::HeaderView; + let best_num = self.chain_info().best_block_number; - let start_num = req.block_num; + let start_num = match req.start { + HashOrNumber::Number(start_num) => start_num, + HashOrNumber::Hash(hash) => match self.block_header(BlockId::Hash(hash)) { + None => { + trace!(target: "les_provider", "Unknown block hash {} requested", hash); + return Vec::new(); + } + Some(header) => { + let num = HeaderView::new(&header).number(); + if req.max == 1 || self.block_hash(BlockId::Number(num)) != Some(hash) { + // Non-canonical header or single header requested. + return vec![header]; + } - match self.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![] + num + } } - } - + }; + (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 }) diff --git a/ethcore/light/src/types/les_request.rs b/ethcore/light/src/types/les_request.rs index 49bd2e9cc..2c7bfb380 100644 --- a/ethcore/light/src/types/les_request.rs +++ b/ethcore/light/src/types/les_request.rs @@ -18,15 +18,34 @@ use util::H256; +/// Either a hash or a number. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "ipc", derive(Binary))] +pub enum HashOrNumber { + /// Block hash variant. + Hash(H256), + /// Block number variant. + Number(u64), +} + +impl From for HashOrNumber { + fn from(hash: H256) -> Self { + HashOrNumber::Hash(hash) + } +} + +impl From for HashOrNumber { + fn from(num: u64) -> Self { + HashOrNumber::Number(num) + } +} + /// A request for block headers. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "ipc", derive(Binary))] pub struct Headers { - /// Starting block number - pub block_num: u64, - /// Starting block hash. This and number could be combined but IPC codegen is - /// not robust enough to support it. - pub block_hash: H256, + /// Starting block number or hash. + pub start: HashOrNumber, /// The maximum amount of headers which can be returned. pub max: usize, /// The amount of headers to skip between each response entry. diff --git a/ethcore/res/ethereum/frontier.json b/ethcore/res/ethereum/frontier.json index 2c520c46a..3a9dce456 100644 --- a/ethcore/res/ethereum/frontier.json +++ b/ethcore/res/ethereum/frontier.json @@ -170,9 +170,6 @@ "enode://cadc6e573b6bc2a9128f2f635ac0db3353e360b56deef239e9be7e7fce039502e0ec670b595f6288c0d2116812516ad6b6ff8d5728ff45eba176989e40dead1e@37.128.191.230:30303", "enode://595a9a06f8b9bc9835c8723b6a82105aea5d55c66b029b6d44f229d6d135ac3ecdd3e9309360a961ea39d7bee7bac5d03564077a4e08823acc723370aace65ec@46.20.235.22:30303", "enode://029178d6d6f9f8026fc0bc17d5d1401aac76ec9d86633bba2320b5eed7b312980c0a210b74b20c4f9a8b0b2bf884b111fa9ea5c5f916bb9bbc0e0c8640a0f56c@216.158.85.185:30303", - "enode://84f5d5957b4880a8b0545e32e05472318898ad9fc8ebe1d56c90c12334a98e12351eccfdf3a2bf72432ac38b57e9d348400d17caa083879ade3822390f89773f@10.1.52.78:30303", - "enode://f90dc9b9bf7b8db97726b7849e175f1eb2707f3d8f281c929336e398dd89b0409fc6aeceb89e846278e9d3ecc3857cebfbe6758ff352ece6fe5d42921ee761db@10.1.173.87:30303", - "enode://6a868ced2dec399c53f730261173638a93a40214cf299ccf4d42a76e3fa54701db410669e8006347a4b3a74fa090bb35af0320e4bc8d04cf5b7f582b1db285f5@10.3.149.199:30303", "enode://fdd1b9bb613cfbc200bba17ce199a9490edc752a833f88d4134bf52bb0d858aa5524cb3ec9366c7a4ef4637754b8b15b5dc913e4ed9fdb6022f7512d7b63f181@212.47.247.103:30303", "enode://a979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@52.16.188.185:30303", "enode://de471bccee3d042261d52e9bff31458daecc406142b401d4cd848f677479f73104b9fdeb090af9583d3391b7f10cb2ba9e26865dd5fca4fcdc0fb1e3b723c786@54.94.239.50:30303", diff --git a/ethcore/src/block.rs b/ethcore/src/block.rs index bcbceb9aa..d37920c3e 100644 --- a/ethcore/src/block.rs +++ b/ethcore/src/block.rs @@ -266,7 +266,8 @@ impl<'x> OpenBlock<'x> { r.block.base.header.set_extra_data(extra_data); r.block.base.header.note_dirty(); - engine.populate_from_parent(&mut r.block.base.header, parent, gas_range_target.0, gas_range_target.1); + let gas_floor_target = ::std::cmp::max(gas_range_target.0, engine.params().min_gas_limit); + engine.populate_from_parent(&mut r.block.base.header, parent, gas_floor_target, gas_range_target.1); engine.on_new_block(&mut r.block); Ok(r) } diff --git a/ethcore/src/engines/mod.rs b/ethcore/src/engines/mod.rs index 8e407f0b7..1aab5f8f4 100644 --- a/ethcore/src/engines/mod.rs +++ b/ethcore/src/engines/mod.rs @@ -125,8 +125,9 @@ pub trait Engine : Sync + Send { self.verify_block_basic(header, None).and_then(|_| self.verify_block_unordered(header, None)) } - /// Don't forget to call Super::populate_from_parent when subclassing & overriding. - // TODO: consider including State in the params. + /// Populate a header's fields based on its parent's header. + /// Takes gas floor and ceiling targets. + /// The gas floor target must not be lower than the engine's minimum gas limit. fn populate_from_parent(&self, header: &mut Header, parent: &Header, _gas_floor_target: U256, _gas_ceil_target: U256) { header.set_difficulty(parent.difficulty().clone()); header.set_gas_limit(parent.gas_limit().clone()); diff --git a/ethcore/src/miner/miner.rs b/ethcore/src/miner/miner.rs index ad541391d..83a96837f 100644 --- a/ethcore/src/miner/miner.rs +++ b/ethcore/src/miner/miner.rs @@ -1092,20 +1092,6 @@ impl MinerService for Miner { fn chain_new_blocks(&self, chain: &MiningBlockChainClient, _imported: &[H256], _invalid: &[H256], enacted: &[H256], retracted: &[H256]) { trace!(target: "miner", "chain_new_blocks"); - fn fetch_transactions(chain: &MiningBlockChainClient, hash: &H256) -> Vec { - let block = chain - .block(BlockId::Hash(*hash)) - // Client should send message after commit to db and inserting to chain. - .expect("Expected in-chain blocks."); - let block = BlockView::new(&block); - let txs = block.transactions(); - // populate sender - for tx in &txs { - let _sender = tx.sender(); - } - txs - } - // 1. We ignore blocks that were `imported` (because it means that they are not in canon-chain, and transactions // should be still available in the queue. // 2. We ignore blocks that are `invalid` because it doesn't have any meaning in terms of the transactions that @@ -1116,35 +1102,29 @@ impl MinerService for Miner { // Then import all transactions... { - let out_of_chain = retracted - .par_iter() - .map(|h| fetch_transactions(chain, h)); - out_of_chain.for_each(|txs| { - let mut transaction_queue = self.transaction_queue.lock(); - let _ = self.add_transactions_to_queue( - chain, txs, TransactionOrigin::RetractedBlock, &mut transaction_queue - ); - }); + retracted.par_iter() + .map(|hash| { + let block = chain.block(BlockId::Hash(*hash)) + .expect("Client is sending message after commit to db and inserting to chain; the block is available; qed"); + let block = BlockView::new(&block); + let txs = block.transactions(); + // populate sender + for tx in &txs { + let _sender = tx.sender(); + } + txs + }).for_each(|txs| { + let mut transaction_queue = self.transaction_queue.lock(); + let _ = self.add_transactions_to_queue( + chain, txs, TransactionOrigin::RetractedBlock, &mut transaction_queue + ); + }); } - // ...and at the end remove old ones + // ...and at the end remove the old ones { - let in_chain = enacted - .par_iter() - .map(|h: &H256| fetch_transactions(chain, h)); - - in_chain.for_each(|mut txs| { - let mut transaction_queue = self.transaction_queue.lock(); - - let to_remove = txs.drain(..) - .map(|tx| { - tx.sender().expect("Transaction is in block, so sender has to be defined.") - }) - .collect::>(); - for sender in to_remove { - transaction_queue.remove_all(sender, chain.latest_nonce(&sender)); - } - }); + let mut transaction_queue = self.transaction_queue.lock(); + transaction_queue.remove_old(|sender| chain.latest_nonce(sender)); } if enacted.len() > 0 { diff --git a/ethcore/src/miner/transaction_queue.rs b/ethcore/src/miner/transaction_queue.rs index cd2d3ba47..b4cc93a9c 100644 --- a/ethcore/src/miner/transaction_queue.rs +++ b/ethcore/src/miner/transaction_queue.rs @@ -79,8 +79,10 @@ //! we check if the transactions should go to `current` (comparing state nonce) //! - When it's removed from `current` - all transactions from this sender (`current` & `future`) are recalculated. //! 3. `remove_all` is used to inform the queue about client (state) nonce changes. -//! - It removes all transactions (either from `current` or `future`) with nonce < client nonce -//! - It moves matching `future` transactions to `current` +//! - It removes all transactions (either from `current` or `future`) with nonce < client nonce +//! - It moves matching `future` transactions to `current` +//! 4. `remove_old` is used as convenient method to update the state nonce for all senders in the queue. +//! - Invokes `remove_all` with latest state nonce for all senders. use std::ops::Deref; use std::cmp::Ordering; @@ -752,6 +754,26 @@ impl TransactionQueue { /// Removes all transactions from particular sender up to (excluding) given client (state) nonce. /// Client (State) Nonce = next valid nonce for this sender. pub fn remove_all(&mut self, sender: Address, client_nonce: U256) { + // Check if there is anything in current... + let should_check_in_current = self.current.by_address.row(&sender) + // If nonce == client_nonce nothing is changed + .and_then(|by_nonce| by_nonce.keys().find(|nonce| *nonce < &client_nonce)) + .map(|_| ()); + // ... or future + let should_check_in_future = self.future.by_address.row(&sender) + // if nonce == client_nonce we need to promote to current + .and_then(|by_nonce| by_nonce.keys().find(|nonce| *nonce <= &client_nonce)) + .map(|_| ()); + + if should_check_in_current.or(should_check_in_future).is_none() { + return; + } + + self.remove_all_internal(sender, client_nonce); + } + + /// Always updates future and moves transactions from current to future. + fn remove_all_internal(&mut self, sender: Address, client_nonce: U256) { // We will either move transaction to future or remove it completely // so there will be no transactions from this sender in current self.last_nonces.remove(&sender); @@ -765,6 +787,20 @@ impl TransactionQueue { assert_eq!(self.future.by_priority.len() + self.current.by_priority.len(), self.by_hash.len()); } + /// Checks the current nonce for all transactions' senders in the queue and removes the old transactions. + pub fn remove_old(&mut self, fetch_nonce: F) where + F: Fn(&Address) -> U256, + { + let senders = self.current.by_address.keys() + .chain(self.future.by_address.keys()) + .cloned() + .collect::>(); + + for sender in senders { + self.remove_all(sender, fetch_nonce(&sender)); + } + } + /// Penalize transactions from sender of transaction with given hash. /// I.e. it should change the priority of the transaction in the queue. /// @@ -847,7 +883,7 @@ impl TransactionQueue { if order.is_some() { // This will keep consistency in queue // Moves all to future and then promotes a batch from current: - self.remove_all(sender, current_nonce); + self.remove_all_internal(sender, current_nonce); assert_eq!(self.future.by_priority.len() + self.current.by_priority.len(), self.by_hash.len()); return; } @@ -2438,7 +2474,7 @@ mod test { } #[test] - fn should_reject_transactions_below_bas_gas() { + fn should_reject_transactions_below_base_gas() { // given let mut txq = TransactionQueue::default(); let (tx1, tx2) = new_tx_pair_default(1.into(), 0.into()); @@ -2457,4 +2493,26 @@ mod test { } + #[test] + fn should_clear_all_old_transactions() { + // given + let mut txq = TransactionQueue::default(); + let (tx1, tx2) = new_tx_pair_default(1.into(), 0.into()); + let (tx3, tx4) = new_tx_pair_default(1.into(), 0.into()); + let nonce1 = tx1.nonce; + + // Insert all transactions + txq.add(tx1, TransactionOrigin::External, &default_account_details, &gas_estimator).unwrap(); + txq.add(tx2, TransactionOrigin::External, &default_account_details, &gas_estimator).unwrap(); + txq.add(tx3, TransactionOrigin::External, &default_account_details, &gas_estimator).unwrap(); + txq.add(tx4, TransactionOrigin::External, &default_account_details, &gas_estimator).unwrap(); + assert_eq!(txq.top_transactions().len(), 4); + + // when + txq.remove_old(|_| nonce1 + U256::one()); + + // then + assert_eq!(txq.top_transactions().len(), 2); + } + } diff --git a/js/npm/etherscan/README.md b/js/npm/etherscan/README.md new file mode 100644 index 000000000..8130db3c6 --- /dev/null +++ b/js/npm/etherscan/README.md @@ -0,0 +1,34 @@ +# @parity/etherscan + +A thin, lightweight promise wrapper for the api.etherscan.io/apis service, exposing a common endpoint for use in JavaScript applications. + +[https://github.com/ethcore/parity/tree/master/js/src/3rdparty/etherscan](https://github.com/ethcore/parity/tree/master/js/src/3rdparty/etherscan) + +## usage + +installation - + +``` +npm install --save @parity/etherscan +``` + +Usage - + +``` +const etherscan = require('@parity/etherscan'); + +// api calls goes here +``` + +## api + +account (exposed on etherscan.account) - + +- `balance(address)` +- `balances(addresses)` (array or addresses) +- `transactions(address, page)` (page offset starts at 0, returns 25) + +stats (exposed on etherscan.stats) - + +- `price()` +- `supply()` diff --git a/js/npm/etherscan/package.json b/js/npm/etherscan/package.json new file mode 100644 index 000000000..0cfdf83e1 --- /dev/null +++ b/js/npm/etherscan/package.json @@ -0,0 +1,33 @@ +{ + "name": "@parity/etherscan", + "description": "The Parity Promise-based library for interfacing with Etherscan over HTTP", + "version": "0.0.0", + "main": "library.js", + "author": "Parity Team ", + "maintainers": [ + "Jaco Greeff" + ], + "contributors": [], + "license": "GPL-3.0", + "repository": { + "type": "git", + "url": "git+https://github.com/ethcore/parity.git" + }, + "keywords": [ + "Ethereum", + "ABI", + "API", + "RPC", + "Parity", + "Promise" + ], + "scripts": { + }, + "devDependencies": { + "chai": "3.5.0", + "mocha": "3.2.0" + }, + "dependencies": { + "node-fetch": "~1.6.3" + } +} diff --git a/js/parity.md b/js/npm/parity/README.md similarity index 92% rename from js/parity.md rename to js/npm/parity/README.md index 3e42f5c8d..30efd3b94 100644 --- a/js/parity.md +++ b/js/npm/parity/README.md @@ -1,7 +1,9 @@ -# parity.js +# @parity/parity.js Parity.js is a thin, fast, Promise-based wrapper around the Ethereum APIs. +[https://github.com/ethcore/parity/tree/master/js/src/api](https://github.com/ethcore/parity/tree/master/js/src/api) + ## installation Install the package with `npm install --save @parity/parity.js` diff --git a/js/parity.package.json b/js/npm/parity/package.json similarity index 72% rename from js/parity.package.json rename to js/npm/parity/package.json index 0974e072f..7b1fcebda 100644 --- a/js/parity.package.json +++ b/js/npm/parity/package.json @@ -1,6 +1,6 @@ { "name": "@parity/parity.js", - "description": "The Parity Promise-base API & ABI library for interfacing with Ethereum over RPC", + "description": "The Parity Promise-based API & ABI library for interfacing with Ethereum over RPC", "version": "0.0.0", "main": "library.js", "author": "Parity Team ", @@ -26,8 +26,8 @@ "devDependencies": { }, "dependencies": { - "bignumber.js": "^2.3.0", - "js-sha3": "^0.5.2", - "node-fetch": "^1.6.3" + "bignumber.js": "~2.3.0", + "js-sha3": "~0.5.2", + "node-fetch": "~1.6.3" } } diff --git a/js/npm/parity/test/smoke.spec.js b/js/npm/parity/test/smoke.spec.js new file mode 100644 index 000000000..9920b10d2 --- /dev/null +++ b/js/npm/parity/test/smoke.spec.js @@ -0,0 +1,26 @@ +// 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 . + +const parity = require('../'); + +describe('load the Parity library', function () { + it('should no throw any error', () => { + expect(parity).to.be.ok; + + expect(parity.Api).to.be.ok; + expect(parity.Abi).to.be.ok; + }); +}); diff --git a/js/npm/shapeshift/README.md b/js/npm/shapeshift/README.md new file mode 100644 index 000000000..0544b6f99 --- /dev/null +++ b/js/npm/shapeshift/README.md @@ -0,0 +1,34 @@ +# @parity/shapeshift + +A thin ES6 promise wrapper around the shapeshift.io APIs as documented at https://shapeshift.io/api + +[https://github.com/ethcore/parity/tree/master/js/src/3rdparty/shapeshift](https://github.com/ethcore/parity/tree/master/js/src/3rdparty/shapeshift) + +## usage + +installation - + +``` +npm install --save @parity/shapeshift +``` + +Usage - + +``` +const APIKEY = 'private affiliate key or undefined'; +const shapeshift = require('@parity/shapeshift')(APIKEY); + +// api calls goes here +``` + +## api + +queries - + +- `getCoins()` [https://shapeshift.io/api#api-104](https://shapeshift.io/api#api-104) +- `getMarketInfo(pair)` [https://shapeshift.io/api#api-103](https://shapeshift.io/api#api-103) +- `getStatus(depositAddress)` [https://shapeshift.io/api#api-5](https://shapeshift.io/api#api-5) + +transactions - + +- `shift(toAddress, returnAddress, pair)` [https://shapeshift.io/api#api-7](https://shapeshift.io/api#api-7) diff --git a/js/npm/shapeshift/package.json b/js/npm/shapeshift/package.json new file mode 100644 index 000000000..b0a2c460a --- /dev/null +++ b/js/npm/shapeshift/package.json @@ -0,0 +1,31 @@ +{ + "name": "@parity/shapeshift", + "description": "The Parity Promise-based library for interfacing with ShapeShift over HTTP", + "version": "0.0.0", + "main": "library.js", + "author": "Parity Team ", + "maintainers": [ + "Jaco Greeff" + ], + "contributors": [], + "license": "GPL-3.0", + "repository": { + "type": "git", + "url": "git+https://github.com/ethcore/parity.git" + }, + "keywords": [ + "Ethereum", + "ABI", + "API", + "RPC", + "Parity", + "Promise" + ], + "scripts": { + }, + "devDependencies": { + }, + "dependencies": { + "node-fetch": "~1.6.3" + } +} diff --git a/js/npm/test/mocha.config.js b/js/npm/test/mocha.config.js new file mode 100644 index 000000000..2b871f518 --- /dev/null +++ b/js/npm/test/mocha.config.js @@ -0,0 +1,29 @@ +// 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 . + +const chai = require('chai'); +// const chaiAsPromised from 'chai-as-promised'; +// const chaiEnzyme from 'chai-enzyme'; +// const sinonChai from 'sinon-chai'; + +// chai.use(chaiAsPromised); +// chai.use(chaiEnzyme()); +// chai.use(sinonChai); + +// expose expect to global so we won't have to manually import & define it in every test +global.expect = chai.expect; + +module.exports = {}; diff --git a/js/npm/test/mocha.opts b/js/npm/test/mocha.opts new file mode 100644 index 000000000..0ed8269b4 --- /dev/null +++ b/js/npm/test/mocha.opts @@ -0,0 +1 @@ +-r ./test/mocha.config diff --git a/js/package.json b/js/package.json index 524c280dc..9f8eabc2c 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.2.105", + "version": "0.2.112", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", @@ -43,7 +43,7 @@ "test": "NODE_ENV=test mocha 'src/**/*.spec.js'", "test:coverage": "NODE_ENV=test istanbul cover _mocha -- 'src/**/*.spec.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/npmParity && (rm -rf .npmjs/node_modules)", "prepush": "npm run lint:cached" }, "devDependencies": { @@ -59,6 +59,7 @@ "babel-plugin-transform-runtime": "6.15.0", "babel-plugin-webpack-alias": "2.1.2", "babel-polyfill": "6.20.0", + "babel-preset-env": "1.0.2", "babel-preset-es2015": "6.18.0", "babel-preset-es2016": "6.16.0", "babel-preset-es2017": "6.16.0", @@ -146,6 +147,7 @@ "mobx-react-devtools": "4.2.10", "moment": "2.17.0", "phoneformat.js": "1.0.3", + "push.js": "0.0.11", "qs": "6.3.0", "react": "15.4.1", "react-ace": "4.1.0", diff --git a/js/scripts/dryrun-npm.sh b/js/scripts/dryrun-npm.sh new file mode 100755 index 000000000..6d9412f62 --- /dev/null +++ b/js/scripts/dryrun-npm.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -e + +# variables +PACKAGES=( "parity" "etherscan" "shapeshift" ) + +# change into the build directory +BASEDIR=`dirname $0` +cd $BASEDIR/.. + +# build all packages +echo "*** Building packages for npmjs" +echo "$NPM_TOKEN" >> ~/.npmrc + +for PACKAGE in ${PACKAGES[@]} +do + echo "*** Building $PACKAGE" + LIBRARY=$PACKAGE npm run ci:build:npm + DIRECTORY=.npmjs/$PACKAGE + + cd $DIRECTORY + echo "*** Executing $PACKAGE tests from $DIRECTORY" + npm test + + echo "*** Publishing $PACKAGE from $DIRECTORY" + echo "npm publish --access public || true" + cd ../.. + +done +cd .. + +# exit with exit code +exit 0 diff --git a/js/scripts/release.sh b/js/scripts/release.sh index 1cf3095ef..88422df0a 100755 --- a/js/scripts/release.sh +++ b/js/scripts/release.sh @@ -3,7 +3,7 @@ set -e # variables UTCDATE=`date -u "+%Y%m%d-%H%M%S"` -PACKAGES=( "parity.js" ) +PACKAGES=( "parity" "etherscan" "shapeshift" ) BRANCH=$CI_BUILD_REF_NAME GIT_JS_PRECOMPILED="https://${GITHUB_JS_PRECOMPILED}:@github.com/ethcore/js-precompiled.git" GIT_PARITY="https://${GITHUB_JS_PRECOMPILED}:@github.com/ethcore/parity.git" @@ -59,19 +59,27 @@ git reset --hard origin/$BRANCH 2>$GITLOG if [ "$BRANCH" == "master" ]; then cd js + echo "*** Bumping package.json patch version" npm --no-git-tag-version version npm version patch echo "*** Building packages for npmjs" - # echo -e "$NPM_USERNAME\n$NPM_PASSWORD\n$NPM_EMAIL" | npm login echo "$NPM_TOKEN" >> ~/.npmrc - npm run ci:build:npm - echo "*** Publishing $PACKAGE to npmjs" - cd .npmjs - npm publish --access public || true - cd ../.. + for PACKAGE in ${PACKAGES[@]} + do + echo "*** Building $PACKAGE" + LIBRARY=$PACKAGE npm run ci:build:npm + DIRECTORY=.npmjs/$PACKAGE + + echo "*** Publishing $PACKAGE from $DIRECTORY" + cd $DIRECTORY + npm publish --access public || true + cd ../.. + done + + cd .. fi echo "*** Updating cargo parity-ui-precompiled#$PRECOMPILED_HASH" diff --git a/js/src/3rdparty/etherscan/account.spec.js b/js/src/3rdparty/etherscan/account.spec.js index 283fab3d2..9dc36c254 100644 --- a/js/src/3rdparty/etherscan/account.spec.js +++ b/js/src/3rdparty/etherscan/account.spec.js @@ -14,11 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import etherscan from './'; +const etherscan = require('./'); const TESTADDR = '0xbf885e2b55c6bcc84556a3c5f07d3040833c8d00'; -describe.skip('etherscan/account', () => { +describe.skip('etherscan/account', function () { + this.timeout(60 * 1000); + const checkBalance = function (balance, addr) { expect(balance).to.be.ok; expect(balance.account).to.equal(addr); diff --git a/js/src/3rdparty/etherscan/stats.spec.js b/js/src/3rdparty/etherscan/stats.spec.js index fde2b035c..62152b6be 100644 --- a/js/src/3rdparty/etherscan/stats.spec.js +++ b/js/src/3rdparty/etherscan/stats.spec.js @@ -14,9 +14,11 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import etherscan from './'; +const etherscan = require('./'); + +describe.skip('etherscan/stats', function () { + this.timeout(60 * 1000); -describe.skip('etherscan/stats', () => { it('retrieves the latest price', () => { return etherscan.stats .price() diff --git a/js/src/3rdparty/shapeshift/helpers.spec.js b/js/src/3rdparty/shapeshift/helpers.spec.js index a82b2f6c3..f2ea0f3f9 100644 --- a/js/src/3rdparty/shapeshift/helpers.spec.js +++ b/js/src/3rdparty/shapeshift/helpers.spec.js @@ -14,22 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import chai from 'chai'; -import nock from 'nock'; +const nock = require('nock'); -global.expect = chai.expect; // eslint-disable-line no-undef - -import 'isomorphic-fetch'; -import es6Promise from 'es6-promise'; -es6Promise.polyfill(); - -import initShapeshift from './'; -import initRpc from './rpc'; +const ShapeShift = require('./'); +const initShapeshift = (ShapeShift.default || ShapeShift); const APIKEY = '0x123454321'; const shapeshift = initShapeshift(APIKEY); -const rpc = initRpc(APIKEY); +const rpc = shapeshift.getRpc(); function mockget (requests) { let scope = nock(rpc.ENDPOINT); @@ -62,7 +55,7 @@ function mockpost (requests) { return scope; } -export { +module.exports = { APIKEY, mockget, mockpost, diff --git a/js/src/3rdparty/shapeshift/rpc.spec.js b/js/src/3rdparty/shapeshift/rpc.spec.js index 8de9e8641..47d4e0052 100644 --- a/js/src/3rdparty/shapeshift/rpc.spec.js +++ b/js/src/3rdparty/shapeshift/rpc.spec.js @@ -14,7 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { APIKEY, mockget, mockpost, rpc } from './helpers.spec.js'; +const helpers = require('./helpers.spec.js'); + +const APIKEY = helpers.APIKEY; +const mockget = helpers.mockget; +const mockpost = helpers.mockpost; +const rpc = helpers.rpc; describe('shapeshift/rpc', () => { describe('GET', () => { diff --git a/js/src/3rdparty/shapeshift/shapeshift.js b/js/src/3rdparty/shapeshift/shapeshift.js index 8f388d0a7..5743fcac1 100644 --- a/js/src/3rdparty/shapeshift/shapeshift.js +++ b/js/src/3rdparty/shapeshift/shapeshift.js @@ -26,6 +26,10 @@ export default function (rpc) { return rpc.get(`marketinfo/${pair}`); } + function getRpc () { + return rpc; + } + function getStatus (depositAddress) { return rpc.get(`txStat/${depositAddress}`); } @@ -103,6 +107,7 @@ export default function (rpc) { return { getCoins, getMarketInfo, + getRpc, getStatus, shift, subscribe, diff --git a/js/src/3rdparty/shapeshift/shapeshift.spec.js b/js/src/3rdparty/shapeshift/shapeshift.spec.js index 36b1506a2..01180e130 100644 --- a/js/src/3rdparty/shapeshift/shapeshift.spec.js +++ b/js/src/3rdparty/shapeshift/shapeshift.spec.js @@ -14,7 +14,11 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { mockget, mockpost, shapeshift } from './helpers.spec.js'; +const helpers = require('./helpers.spec.js'); + +const mockget = helpers.mockget; +const mockpost = helpers.mockpost; +const shapeshift = helpers.shapeshift; describe('shapeshift/calls', () => { describe('getCoins', () => { diff --git a/js/src/contracts/contracts.js b/js/src/contracts/contracts.js index f61a63690..a8020b825 100644 --- a/js/src/contracts/contracts.js +++ b/js/src/contracts/contracts.js @@ -62,6 +62,10 @@ export default class Contracts { } static create (api) { + if (instance) { + return instance; + } + return new Contracts(api); } diff --git a/js/src/index.js b/js/src/index.js index 6938a46f8..46d6c9c74 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -67,7 +67,7 @@ if (window.location.hash && window.location.hash.indexOf(AUTH_HASH) === 0) { const api = new SecureApi(`ws://${parityUrl}`, token); ContractInstances.create(api); -const store = initStore(api); +const store = initStore(api, hashHistory); store.dispatch({ type: 'initAll', api }); store.dispatch(setApi(api)); diff --git a/js/src/library.etherscan.js b/js/src/library.etherscan.js new file mode 100644 index 000000000..59cb861d3 --- /dev/null +++ b/js/src/library.etherscan.js @@ -0,0 +1,34 @@ +// 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 . + +import 'babel-polyfill/dist/polyfill.js'; +import es6Promise from 'es6-promise'; +es6Promise.polyfill(); + +const isNode = typeof global !== 'undefined' && typeof global !== 'undefined'; +const isBrowser = typeof self !== 'undefined' && typeof self.window !== 'undefined'; + +if (isBrowser) { + require('whatwg-fetch'); +} + +if (isNode) { + global.fetch = require('node-fetch'); +} + +import Etherscan from './3rdparty/etherscan'; + +module.exports = Etherscan; diff --git a/js/src/library.js b/js/src/library.parity.js similarity index 100% rename from js/src/library.js rename to js/src/library.parity.js diff --git a/js/src/library.shapeshift.js b/js/src/library.shapeshift.js new file mode 100644 index 000000000..3f24f216b --- /dev/null +++ b/js/src/library.shapeshift.js @@ -0,0 +1,34 @@ +// 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 . + +import 'babel-polyfill/dist/polyfill.js'; +import es6Promise from 'es6-promise'; +es6Promise.polyfill(); + +const isNode = typeof global !== 'undefined' && typeof global !== 'undefined'; +const isBrowser = typeof self !== 'undefined' && typeof self.window !== 'undefined'; + +if (isBrowser) { + require('whatwg-fetch'); +} + +if (isNode) { + global.fetch = require('node-fetch'); +} + +import ShapeShift from './3rdparty/shapeshift'; + +module.exports = ShapeShift; diff --git a/js/src/main.js b/js/src/main.js index d508c50fc..f61e3d563 100644 --- a/js/src/main.js +++ b/js/src/main.js @@ -15,7 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; -import { Redirect, Router, Route } from 'react-router'; +import { Redirect, Router, Route, IndexRoute } from 'react-router'; import { Accounts, Account, Addresses, Address, Application, Contract, Contracts, WriteContract, Wallet, Dapp, Dapps, Settings, SettingsBackground, SettingsParity, SettingsProxy, SettingsViews, Signer, Status } from '~/views'; @@ -26,6 +26,23 @@ export default class MainApplication extends Component { routerHistory: PropTypes.any.isRequired }; + handleDeprecatedRoute = (nextState, replace) => { + const { address } = nextState.params; + const redirectMap = { + account: 'accounts', + address: 'addresses', + contract: 'contracts' + }; + + const oldRoute = nextState.routes[0].path; + const newRoute = Object.keys(redirectMap).reduce((newRoute, key) => { + return newRoute.replace(new RegExp(`^/${key}`), '/' + redirectMap[key]); + }, oldRoute); + + console.warn(`Route "${oldRoute}" is deprecated. Please use "${newRoute}"`); + replace(newRoute.replace(':address', address)); + } + render () { const { routerHistory } = this.props; @@ -34,26 +51,46 @@ export default class MainApplication extends Component { + + { /** Backward Compatible links */ } + + + + - - - - - + + + + + + + + + + + - - - + + + + + + + + - - + + + + + ); diff --git a/js/src/modals/AddAddress/addAddress.js b/js/src/modals/AddAddress/addAddress.js index e44cb0b3c..a72158cc7 100644 --- a/js/src/modals/AddAddress/addAddress.js +++ b/js/src/modals/AddAddress/addAddress.js @@ -28,6 +28,7 @@ export default class AddAddress extends Component { static propTypes = { contacts: PropTypes.object.isRequired, + address: PropTypes.string, onClose: PropTypes.func }; @@ -39,6 +40,12 @@ export default class AddAddress extends Component { description: '' }; + componentWillMount () { + if (this.props.address) { + this.onEditAddress(null, this.props.address); + } + } + render () { return ( + balances={ balances } + error={ fromAddressError } + onChange={ this.onFromAddressChange } + value={ fromAddress } + /> + /> + /> { this.renderContractSelect() } @@ -119,17 +123,19 @@ export default class DetailsStep extends Component { label='abi / solc combined-output' hint='the abi of the contract to deploy or solc combined-output' error={ abiError } - value={ solcOutput } onChange={ this.onSolcChange } onSubmit={ this.onSolcSubmit } - readOnly={ readOnly } /> + readOnly={ readOnly } + value={ solcOutput } + /> + readOnly={ readOnly || solc } + value={ code } + /> ); diff --git a/js/src/modals/DeployContract/deployContract.js b/js/src/modals/DeployContract/deployContract.js index 5bf4fc389..21325f786 100644 --- a/js/src/modals/DeployContract/deployContract.js +++ b/js/src/modals/DeployContract/deployContract.js @@ -15,8 +15,10 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; import ContentClear from 'material-ui/svg-icons/content/clear'; +import { pick } from 'lodash'; import { BusyStep, CompletedStep, CopyToClipboard, Button, IdentityIcon, Modal, TxHash } from '~/ui'; import { ERRORS, validateAbi, validateCode, validateName } from '~/util/validation'; @@ -36,7 +38,7 @@ const STEPS = { COMPLETED: { title: 'completed' } }; -export default class DeployContract extends Component { +class DeployContract extends Component { static contextTypes = { api: PropTypes.object.isRequired, store: PropTypes.object.isRequired @@ -45,6 +47,7 @@ export default class DeployContract extends Component { static propTypes = { accounts: PropTypes.object.isRequired, onClose: PropTypes.func.isRequired, + balances: PropTypes.object, abi: PropTypes.string, code: PropTypes.string, readOnly: PropTypes.bool, @@ -192,7 +195,7 @@ export default class DeployContract extends Component { } renderStep () { - const { accounts, readOnly } = this.props; + const { accounts, readOnly, balances } = this.props; const { address, deployError, step, deployState, txhash, rejected } = this.state; if (deployError) { @@ -216,6 +219,7 @@ export default class DeployContract extends Component { { + const balances = pick(state.balances.balances, fromAddresses); + return { balances }; + }; +} + +export default connect( + mapStateToProps +)(DeployContract); + diff --git a/js/src/modals/ExecuteContract/DetailsStep/detailsStep.js b/js/src/modals/ExecuteContract/DetailsStep/detailsStep.js index 3ffb929a9..fde7fa1b2 100644 --- a/js/src/modals/ExecuteContract/DetailsStep/detailsStep.js +++ b/js/src/modals/ExecuteContract/DetailsStep/detailsStep.js @@ -32,25 +32,27 @@ export default class DetailsStep extends Component { static propTypes = { accounts: PropTypes.object.isRequired, contract: PropTypes.object.isRequired, - amount: PropTypes.string, - amountError: PropTypes.string, onAmountChange: PropTypes.func.isRequired, - fromAddress: PropTypes.string, - fromAddressError: PropTypes.string, - gasEdit: PropTypes.bool, onFromAddressChange: PropTypes.func.isRequired, - func: PropTypes.object, - funcError: PropTypes.string, - onFuncChange: PropTypes.func, - onGasEditClick: PropTypes.func, + onValueChange: PropTypes.func.isRequired, values: PropTypes.array.isRequired, valuesError: PropTypes.array.isRequired, - warning: PropTypes.string, - onValueChange: PropTypes.func.isRequired + + amount: PropTypes.string, + amountError: PropTypes.string, + balances: PropTypes.object, + fromAddress: PropTypes.string, + fromAddressError: PropTypes.string, + func: PropTypes.object, + funcError: PropTypes.string, + gasEdit: PropTypes.bool, + onFuncChange: PropTypes.func, + onGasEditClick: PropTypes.func, + warning: PropTypes.string } render () { - const { accounts, amount, amountError, fromAddress, fromAddressError, gasEdit, onGasEditClick, onFromAddressChange, onAmountChange } = this.props; + const { accounts, amount, amountError, balances, fromAddress, fromAddressError, gasEdit, onGasEditClick, onFromAddressChange, onAmountChange } = this.props; return (
@@ -61,6 +63,7 @@ export default class DetailsStep extends Component { value={ fromAddress } error={ fromAddressError } accounts={ accounts } + balances={ balances } onChange={ onFromAddressChange } /> { this.renderFunctionSelect() } { this.renderParameters() } diff --git a/js/src/modals/ExecuteContract/executeContract.js b/js/src/modals/ExecuteContract/executeContract.js index 7b4e8ccd2..c3ac96490 100644 --- a/js/src/modals/ExecuteContract/executeContract.js +++ b/js/src/modals/ExecuteContract/executeContract.js @@ -18,6 +18,8 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { observer } from 'mobx-react'; +import { pick } from 'lodash'; + import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; import ContentClear from 'material-ui/svg-icons/content/clear'; import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back'; @@ -57,6 +59,7 @@ class ExecuteContract extends Component { isTest: PropTypes.bool, fromAddress: PropTypes.string, accounts: PropTypes.object, + balances: PropTypes.object, contract: PropTypes.object, gasLimit: PropTypes.object.isRequired, onClose: PropTypes.func.isRequired, @@ -362,10 +365,15 @@ class ExecuteContract extends Component { } } -function mapStateToProps (state) { - const { gasLimit } = state.nodeStatus; +function mapStateToProps (initState, initProps) { + const fromAddresses = Object.keys(initProps.accounts); - return { gasLimit }; + return (state) => { + const balances = pick(state.balances.balances, fromAddresses); + const { gasLimit } = state.nodeStatus; + + return { gasLimit, balances }; + }; } function mapDispatchToProps (dispatch) { diff --git a/js/src/modals/Transfer/Details/details.js b/js/src/modals/Transfer/Details/details.js index 53f04c489..20ac06f85 100644 --- a/js/src/modals/Transfer/Details/details.js +++ b/js/src/modals/Transfer/Details/details.js @@ -134,6 +134,7 @@ export default class Details extends Component { images: PropTypes.object.isRequired, sender: PropTypes.string, senderError: PropTypes.string, + sendersBalances: PropTypes.object, recipient: PropTypes.string, recipientError: PropTypes.string, tag: PropTypes.string, @@ -203,7 +204,7 @@ export default class Details extends Component { } renderFromAddress () { - const { sender, senderError, senders } = this.props; + const { sender, senderError, senders, sendersBalances } = this.props; if (!senders) { return null; @@ -218,6 +219,7 @@ export default class Details extends Component { hint='the sender address' value={ sender } onChange={ this.onEditSender } + balances={ sendersBalances } /> ); diff --git a/js/src/modals/Transfer/store.js b/js/src/modals/Transfer/store.js index 3a8f54f92..e08d7203d 100644 --- a/js/src/modals/Transfer/store.js +++ b/js/src/modals/Transfer/store.js @@ -54,6 +54,7 @@ export default class TransferStore { @observable sender = ''; @observable senderError = null; + @observable sendersBalances = {}; @observable total = '0.0'; @observable totalError = null; @@ -66,8 +67,6 @@ export default class TransferStore { onClose = null; senders = null; - sendersBalances = null; - isWallet = false; wallet = null; diff --git a/js/src/modals/Transfer/transfer.js b/js/src/modals/Transfer/transfer.js index 0c96a1168..57dc569f2 100644 --- a/js/src/modals/Transfer/transfer.js +++ b/js/src/modals/Transfer/transfer.js @@ -155,8 +155,8 @@ class Transfer extends Component { renderDetailsPage () { const { account, balance, images, senders } = this.props; - const { valueAll, extras, recipient, recipientError, sender, senderError } = this.store; - const { tag, total, totalError, value, valueError } = this.store; + const { recipient, recipientError, sender, senderError, sendersBalances } = this.store; + const { valueAll, extras, tag, total, totalError, value, valueError } = this.store; return (
. import thunk from 'redux-thunk'; +import { routerMiddleware } from 'react-router-redux'; import ErrorsMiddleware from '~/ui/Errors/middleware'; import SettingsMiddleware from '~/views/Settings/middleware'; @@ -22,12 +23,13 @@ import SignerMiddleware from './providers/signerMiddleware'; import statusMiddleware from '~/views/Status/middleware'; import CertificationsMiddleware from './providers/certifications/middleware'; -export default function (api) { +export default function (api, browserHistory) { const errors = new ErrorsMiddleware(); const signer = new SignerMiddleware(api); const settings = new SettingsMiddleware(); const status = statusMiddleware(); const certifications = new CertificationsMiddleware(); + const routeMiddleware = routerMiddleware(browserHistory); const middleware = [ settings.toMiddleware(), @@ -36,5 +38,5 @@ export default function (api) { certifications.toMiddleware() ]; - return middleware.concat(status, thunk); + return middleware.concat(status, routeMiddleware, thunk); } diff --git a/js/src/redux/providers/balancesActions.js b/js/src/redux/providers/balancesActions.js index 65f831008..f8cfb2c1e 100644 --- a/js/src/redux/providers/balancesActions.js +++ b/js/src/redux/providers/balancesActions.js @@ -15,11 +15,14 @@ // along with Parity. If not, see . import { range, uniq, isEqual } from 'lodash'; +import BigNumber from 'bignumber.js'; +import { push } from 'react-router-redux'; import { hashToImageUrl } from './imagesReducer'; import { setAddressImage } from './imagesActions'; import * as ABIS from '~/contracts/abi'; +import { notifyTransaction } from '~/util/notifications'; import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png'; const ETH = { @@ -28,7 +31,64 @@ const ETH = { image: imagesEthereum }; -export function setBalances (balances) { +function setBalances (_balances) { + return (dispatch, getState) => { + const state = getState(); + + const accounts = state.personal.accounts; + const nextBalances = _balances; + const prevBalances = state.balances.balances; + const balances = { ...prevBalances }; + + Object.keys(nextBalances).forEach((address) => { + if (!balances[address]) { + balances[address] = Object.assign({}, nextBalances[address]); + return; + } + + const balance = Object.assign({}, balances[address]); + const { tokens, txCount = balance.txCount } = nextBalances[address]; + const nextTokens = [].concat(balance.tokens); + + tokens.forEach((t) => { + const { token, value } = t; + const { tag } = token; + + const tokenIndex = nextTokens.findIndex((tok) => tok.token.tag === tag); + + if (tokenIndex === -1) { + nextTokens.push({ + token, + value + }); + } else { + const oldValue = nextTokens[tokenIndex].value; + + // If received a token/eth (old value < new value), notify + if (oldValue.lt(value) && accounts[address]) { + const account = accounts[address]; + const txValue = value.minus(oldValue); + + const redirectToAccount = () => { + const route = `/account/${account.address}`; + dispatch(push(route)); + }; + + notifyTransaction(account, token, txValue, redirectToAccount); + } + + nextTokens[tokenIndex] = { token, value }; + } + }); + + balances[address] = { txCount: txCount || new BigNumber(0), tokens: nextTokens }; + }); + + dispatch(_setBalances(balances)); + }; +} + +function _setBalances (balances) { return { type: 'setBalances', balances @@ -123,14 +183,14 @@ export function fetchBalances (_addresses) { const fullFetch = addresses.length === 1; - const fetchedAddresses = uniq(addresses.concat(Object.keys(accounts))); + const addressesToFetch = uniq(addresses.concat(Object.keys(accounts))); return Promise - .all(fetchedAddresses.map((addr) => fetchAccount(addr, api, fullFetch))) + .all(addressesToFetch.map((addr) => fetchAccount(addr, api, fullFetch))) .then((accountsBalances) => { const balances = {}; - fetchedAddresses.forEach((addr, idx) => { + addressesToFetch.forEach((addr, idx) => { balances[addr] = accountsBalances[idx]; }); @@ -146,10 +206,12 @@ export function fetchBalances (_addresses) { export function updateTokensFilter (_addresses, _tokens) { return (dispatch, getState) => { const { api, balances, personal } = getState(); - const { visibleAccounts } = personal; + const { visibleAccounts, accounts } = personal; const { tokensFilter } = balances; - const addresses = uniq(_addresses || visibleAccounts || []).sort(); + const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts))); + const addresses = uniq(_addresses || addressesToFetch || []).sort(); + const tokens = _tokens || Object.values(balances.tokens) || []; const tokenAddresses = tokens.map((t) => t.address).sort(); @@ -221,8 +283,10 @@ export function updateTokensFilter (_addresses, _tokens) { export function queryTokensFilter (tokensFilter) { return (dispatch, getState) => { const { api, personal, balances } = getState(); - const { visibleAccounts } = personal; + const { visibleAccounts, accounts } = personal; + const visibleAddresses = visibleAccounts.map((a) => a.toLowerCase()); + const addressesToFetch = uniq(visibleAddresses.concat(Object.keys(accounts))); Promise .all([ @@ -237,18 +301,16 @@ export function queryTokensFilter (tokensFilter) { .concat(logsTo) .forEach((log) => { const tokenAddress = log.address; + const fromAddress = '0x' + log.topics[1].slice(-40); const toAddress = '0x' + log.topics[2].slice(-40); - const fromIdx = visibleAddresses.indexOf(fromAddress); - const toIdx = visibleAddresses.indexOf(toAddress); - - if (fromIdx > -1) { - addresses.push(visibleAccounts[fromIdx]); + if (addressesToFetch.includes(fromAddress)) { + addresses.push(fromAddress); } - if (toIdx > -1) { - addresses.push(visibleAccounts[toIdx]); + if (addressesToFetch.includes(toAddress)) { + addresses.push(toAddress); } tokenAddresses.push(tokenAddress); @@ -269,9 +331,10 @@ export function queryTokensFilter (tokensFilter) { export function fetchTokensBalances (_addresses = null, _tokens = null) { return (dispatch, getState) => { const { api, personal, balances } = getState(); - const { visibleAccounts } = personal; + const { visibleAccounts, accounts } = personal; - const addresses = _addresses || visibleAccounts; + const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts))); + const addresses = _addresses || addressesToFetch; const tokens = _tokens || Object.values(balances.tokens); if (addresses.length === 0) { diff --git a/js/src/redux/providers/balancesReducer.js b/js/src/redux/providers/balancesReducer.js index 01923a4f4..4b6950498 100644 --- a/js/src/redux/providers/balancesReducer.js +++ b/js/src/redux/providers/balancesReducer.js @@ -15,7 +15,6 @@ // along with Parity. If not, see . import { handleActions } from 'redux-actions'; -import BigNumber from 'bignumber.js'; const initialState = { balances: {}, @@ -26,39 +25,7 @@ const initialState = { export default handleActions({ setBalances (state, action) { - const nextBalances = action.balances; - const prevBalances = state.balances; - const balances = { ...prevBalances }; - - Object.keys(nextBalances).forEach((address) => { - if (!balances[address]) { - balances[address] = Object.assign({}, nextBalances[address]); - return; - } - - const balance = Object.assign({}, balances[address]); - const { tokens, txCount = balance.txCount } = nextBalances[address]; - const nextTokens = [].concat(balance.tokens); - - tokens.forEach((t) => { - const { token, value } = t; - const { tag } = token; - - const tokenIndex = nextTokens.findIndex((tok) => tok.token.tag === tag); - - if (tokenIndex === -1) { - nextTokens.push({ - token, - value - }); - } else { - nextTokens[tokenIndex] = { token, value }; - } - }); - - balances[address] = Object.assign({}, { txCount: txCount || new BigNumber(0), tokens: nextTokens }); - }); - + const { balances } = action; return Object.assign({}, state, { balances }); }, diff --git a/js/src/redux/providers/index.js b/js/src/redux/providers/index.js index 563378caa..a90d8b62c 100644 --- a/js/src/redux/providers/index.js +++ b/js/src/redux/providers/index.js @@ -21,11 +21,11 @@ export Status from './status'; export apiReducer from './apiReducer'; export balancesReducer from './balancesReducer'; +export blockchainReducer from './blockchainReducer'; +export compilerReducer from './compilerReducer'; export imagesReducer from './imagesReducer'; export personalReducer from './personalReducer'; export signerReducer from './signerReducer'; -export statusReducer from './statusReducer'; -export blockchainReducer from './blockchainReducer'; -export compilerReducer from './compilerReducer'; export snackbarReducer from './snackbarReducer'; +export statusReducer from './statusReducer'; export walletReducer from './walletReducer'; diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index 830192bbe..138479716 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -54,7 +54,10 @@ export default class Status { this._api.eth .getBlockByNumber(blockNumber) .then((block) => { - this._store.dispatch(statusCollection({ gasLimit: block.gasLimit })); + this._store.dispatch(statusCollection({ + blockTimestamp: block.timestamp, + gasLimit: block.gasLimit + })); }) .catch((error) => { console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error); diff --git a/js/src/redux/providers/statusReducer.js b/js/src/redux/providers/statusReducer.js index 07ba4af5b..279a9da42 100644 --- a/js/src/redux/providers/statusReducer.js +++ b/js/src/redux/providers/statusReducer.js @@ -19,6 +19,7 @@ import { handleActions } from 'redux-actions'; const initialState = { blockNumber: new BigNumber(0), + blockTimestamp: new Date(), devLogs: [], devLogsLevels: null, devLogsEnabled: false, diff --git a/js/src/redux/reducers.js b/js/src/redux/reducers.js index 92388df65..642dfe403 100644 --- a/js/src/redux/reducers.js +++ b/js/src/redux/reducers.js @@ -17,7 +17,12 @@ import { combineReducers } from 'redux'; import { routerReducer } from 'react-router-redux'; -import { apiReducer, balancesReducer, blockchainReducer, compilerReducer, imagesReducer, personalReducer, signerReducer, statusReducer as nodeStatusReducer, snackbarReducer, walletReducer } from './providers'; +import { + apiReducer, balancesReducer, blockchainReducer, + compilerReducer, imagesReducer, personalReducer, + signerReducer, statusReducer as nodeStatusReducer, + snackbarReducer, walletReducer +} from './providers'; import certificationsReducer from './providers/certifications/reducer'; import errorReducer from '~/ui/Errors/reducers'; diff --git a/js/src/redux/store.js b/js/src/redux/store.js index 2ff50ea53..1d62f9ea5 100644 --- a/js/src/redux/store.js +++ b/js/src/redux/store.js @@ -32,9 +32,9 @@ const storeCreation = window.devToolsExtension ? window.devToolsExtension()(createStore) : createStore; -export default function (api) { +export default function (api, browserHistory) { const reducers = initReducers(); - const middleware = initMiddleware(api); + const middleware = initMiddleware(api, browserHistory); const store = applyMiddleware(...middleware)(storeCreation)(reducers); new BalancesProvider(store, api).start(); diff --git a/js/src/ui/Actionbar/actionbar.js b/js/src/ui/Actionbar/actionbar.js index c3fbe6a18..f744f5c57 100644 --- a/js/src/ui/Actionbar/actionbar.js +++ b/js/src/ui/Actionbar/actionbar.js @@ -17,11 +17,13 @@ import React, { Component, PropTypes } from 'react'; import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar'; +import { nodeOrStringProptype } from '~/util/proptypes'; + import styles from './actionbar.css'; export default class Actionbar extends Component { static propTypes = { - title: PropTypes.string, + title: nodeOrStringProptype(), buttons: PropTypes.array, children: PropTypes.node, className: PropTypes.string diff --git a/js/src/ui/Form/AddressSelect/addressSelect.css b/js/src/ui/Form/AddressSelect/addressSelect.css index 30671db73..01bc8901d 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.css +++ b/js/src/ui/Form/AddressSelect/addressSelect.css @@ -15,7 +15,9 @@ /* along with Parity. If not, see . */ .account { - padding: 4px 0 0 0; + padding: 0.25em 0; + display: flex; + align-items: center; } .name { @@ -27,6 +29,11 @@ padding: 0 0 0 1em; } +.balance { + color: #aaa; + padding-left: 1em; +} + .image { display: inline-block; height: 32px; diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index d0f331c34..0cd92c5c8 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -21,6 +21,8 @@ import AutoComplete from '../AutoComplete'; import IdentityIcon from '../../IdentityIcon'; import IdentityName from '../../IdentityName'; +import { fromWei } from '~/api/util/wei'; + import styles from './addressSelect.css'; export default class AddressSelect extends Component { @@ -40,27 +42,46 @@ export default class AddressSelect extends Component { value: PropTypes.string, tokens: PropTypes.object, onChange: PropTypes.func.isRequired, - allowInput: PropTypes.bool + allowInput: PropTypes.bool, + balances: PropTypes.object } state = { + autocompleteEntries: [], entries: {}, addresses: [], value: '' } entriesFromProps (props = this.props) { - const { accounts, contacts, contracts, wallets } = props; - const entries = Object.assign({}, accounts || {}, wallets || {}, contacts || {}, contracts || {}); - return entries; + const { accounts = {}, contacts = {}, contracts = {}, wallets = {} } = props; + + const autocompleteEntries = [].concat( + Object.values(wallets), + 'divider', + Object.values(accounts), + 'divider', + Object.values(contacts), + 'divider', + Object.values(contracts) + ); + + const entries = { + ...wallets, + ...accounts, + ...contacts, + ...contracts + }; + + return { autocompleteEntries, entries }; } componentWillMount () { const { value } = this.props; - const entries = this.entriesFromProps(); + const { entries, autocompleteEntries } = this.entriesFromProps(); const addresses = Object.keys(entries).sort(); - this.setState({ entries, addresses, value }); + this.setState({ autocompleteEntries, entries, addresses, value }); } componentWillReceiveProps (newProps) { @@ -71,7 +92,7 @@ export default class AddressSelect extends Component { render () { const { allowInput, disabled, error, hint, label } = this.props; - const { entries, value } = this.state; + const { autocompleteEntries, value } = this.state; const searchText = this.getSearchText(); const icon = this.renderIdentityIcon(value); @@ -89,7 +110,7 @@ export default class AddressSelect extends Component { onUpdateInput={ allowInput && this.onUpdateInput } value={ searchText } filter={ this.handleFilter } - entries={ entries } + entries={ autocompleteEntries } entry={ this.getEntry() || {} } renderItem={ this.renderItem } /> @@ -129,7 +150,34 @@ export default class AddressSelect extends Component { }; } + renderBalance (address) { + const { balances = {} } = this.props; + const balance = balances[address]; + + if (!balance) { + return null; + } + + const ethToken = balance.tokens.find((tok) => tok.token && tok.token.tag && tok.token.tag.toLowerCase() === 'eth'); + + if (!ethToken) { + return null; + } + + const value = fromWei(ethToken.value); + + return ( +
+ { value.toFormat(3) } { 'ETH' } +
+ ); + } + renderMenuItem (address) { + const balance = this.props.balances + ? this.renderBalance(address) + : null; + const item = (
+ { balance }
); @@ -155,11 +204,10 @@ export default class AddressSelect extends Component { getSearchText () { const entry = this.getEntry(); - const { value } = this.state; return entry && entry.name ? entry.name.toUpperCase() - : value; + : this.state.value; } getEntry () { diff --git a/js/src/ui/Form/AutoComplete/autocomplete.js b/js/src/ui/Form/AutoComplete/autocomplete.js index c7a5dd141..d11ae7cc5 100644 --- a/js/src/ui/Form/AutoComplete/autocomplete.js +++ b/js/src/ui/Form/AutoComplete/autocomplete.js @@ -16,11 +16,24 @@ import React, { Component, PropTypes } from 'react'; import keycode from 'keycode'; -import { MenuItem, AutoComplete as MUIAutoComplete } from 'material-ui'; +import { MenuItem, AutoComplete as MUIAutoComplete, Divider as MUIDivider } from 'material-ui'; import { PopoverAnimationVertical } from 'material-ui/Popover'; import { isEqual } from 'lodash'; +// Hack to prevent "Unknown prop `disableFocusRipple` on
tag" error +class Divider extends Component { + static muiName = MUIDivider.muiName; + + render () { + return ( +
+ +
+ ); + } +} + export default class AutoComplete extends Component { static propTypes = { onChange: PropTypes.func.isRequired, @@ -38,15 +51,17 @@ export default class AutoComplete extends Component { PropTypes.array, PropTypes.object ]) - } + }; state = { lastChangedValue: undefined, entry: null, open: false, - fakeBlur: false, - dataSource: [] - } + dataSource: [], + dividerBreaks: [] + }; + + dividersVisibility = {}; componentWillMount () { const dataSource = this.getDataSource(); @@ -64,7 +79,7 @@ export default class AutoComplete extends Component { } render () { - const { disabled, error, hint, label, value, className, filter, onUpdateInput } = this.props; + const { disabled, error, hint, label, value, className, onUpdateInput } = this.props; const { open, dataSource } = this.state; return ( @@ -78,9 +93,9 @@ export default class AutoComplete extends Component { onUpdateInput={ onUpdateInput } searchText={ value } onFocus={ this.onFocus } - onBlur={ this.onBlur } + onClose={ this.onClose } animation={ PopoverAnimationVertical } - filter={ filter } + filter={ this.handleFilter } popoverProps={ { open } } openOnFocus menuCloseDelay={ 0 } @@ -100,18 +115,76 @@ export default class AutoComplete extends Component { ? entries : Object.values(entries); - if (renderItem && typeof renderItem === 'function') { - return entriesArray.map(entry => renderItem(entry)); + let currentDivider = 0; + let firstSet = false; + + const dataSource = entriesArray.map((entry, index) => { + // Render divider + if (typeof entry === 'string' && entry.toLowerCase() === 'divider') { + // Don't add divider if nothing before + if (!firstSet) { + return undefined; + } + + const item = { + text: '', + divider: currentDivider, + isDivider: true, + value: ( + + ) + }; + + currentDivider++; + return item; + } + + let item; + + if (renderItem && typeof renderItem === 'function') { + item = renderItem(entry); + } else { + item = { + text: entry, + value: ( + + ) + }; + } + + if (!firstSet) { + item.first = true; + firstSet = true; + } + + item.divider = currentDivider; + + return item; + }).filter((item) => item !== undefined); + + return dataSource; + } + + handleFilter = (searchText, name, item) => { + if (item.isDivider) { + return this.dividersVisibility[item.divider]; } - return entriesArray.map(entry => ({ - text: entry, - value: ( - - ) - })); + if (item.first) { + this.dividersVisibility = {}; + } + + const { filter } = this.props; + const show = filter(searchText, name, item); + + // Show the related divider + if (show) { + this.dividersVisibility[item.divider] = true; + } + + return show; } onKeyDown = (event) => { @@ -121,7 +194,6 @@ export default class AutoComplete extends Component { case 'down': const { menu } = muiAutocomplete.refs; menu && menu.handleKeyDown(event); - this.setState({ fakeBlur: true }); break; case 'enter': @@ -155,22 +227,12 @@ export default class AutoComplete extends Component { this.setState({ entry, open: false }); } - onBlur = (event) => { + onClose = (event) => { const { onUpdateInput } = this.props; - // TODO: Handle blur gracefully where we use onUpdateInput (currently replaces - // input where text is allowed with the last selected value from the dropdown) if (!onUpdateInput) { - window.setTimeout(() => { - const { entry, fakeBlur } = this.state; - - if (fakeBlur) { - this.setState({ fakeBlur: false }); - return; - } - - this.handleOnChange(entry); - }, 200); + const { entry } = this.state; + this.handleOnChange(entry); } } diff --git a/js/src/ui/Page/page.css b/js/src/ui/Page/page.css index 72a78dc22..06d4f7274 100644 --- a/js/src/ui/Page/page.css +++ b/js/src/ui/Page/page.css @@ -16,9 +16,9 @@ */ .layout { - padding: 0.25em 0.25em 1em 0.25em; + padding: 0.25em; - > * { + &>div { margin-bottom: 0.75em; } } diff --git a/js/src/ui/Page/page.js b/js/src/ui/Page/page.js index 867c3fdf1..9646358b3 100644 --- a/js/src/ui/Page/page.js +++ b/js/src/ui/Page/page.js @@ -16,21 +16,38 @@ import React, { Component, PropTypes } from 'react'; +import Actionbar from '../Actionbar'; +import { nodeOrStringProptype } from '~/util/proptypes'; + import styles from './page.css'; export default class Page extends Component { static propTypes = { + buttons: PropTypes.array, className: PropTypes.string, - children: PropTypes.node + children: PropTypes.node, + title: nodeOrStringProptype() }; render () { - const { className, children } = this.props; + const { buttons, className, children, title } = this.props; const classes = `${styles.layout} ${className}`; + let actionbar = null; + + if (title || buttons) { + actionbar = ( + + ); + } return ( -
- { children } +
+ { actionbar } +
+ { children } +
); } diff --git a/js/src/util/notifications.js b/js/src/util/notifications.js new file mode 100644 index 000000000..479448234 --- /dev/null +++ b/js/src/util/notifications.js @@ -0,0 +1,45 @@ +// 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 . + +import Push from 'push.js'; +import BigNumber from 'bignumber.js'; +import { noop } from 'lodash'; + +import { fromWei } from '~/api/util/wei'; + +import ethereumIcon from '~/../assets/images/contracts/ethereum-black-64x64.png'; +import unkownIcon from '~/../assets/images/contracts/unknown-64x64.png'; + +export function notifyTransaction (account, token, _value, onClick) { + const name = account.name || account.address; + const value = token.tag.toLowerCase() === 'eth' + ? fromWei(_value) + : _value.div(new BigNumber(token.format || 1)); + + const icon = token.tag.toLowerCase() === 'eth' + ? ethereumIcon + : (token.image || unkownIcon); + + Push.create(`${name}`, { + body: `You just received ${value.toFormat()} ${token.tag.toUpperCase()}`, + icon: { + x16: icon, + x32: icon + }, + timeout: 20000, + onClick: onClick || noop + }); +} diff --git a/js/src/views/Account/Header/header.css b/js/src/views/Account/Header/header.css index 74390face..26c2a5b22 100644 --- a/js/src/views/Account/Header/header.css +++ b/js/src/views/Account/Header/header.css @@ -31,6 +31,10 @@ .infoline, .uuidline { line-height: 1.618em; + + &.bigaddress { + font-size: 1.25em; + } } .infoline, diff --git a/js/src/views/Account/Header/header.js b/js/src/views/Account/Header/header.js index 6ce88aef4..7df87bd9c 100644 --- a/js/src/views/Account/Header/header.js +++ b/js/src/views/Account/Header/header.js @@ -32,18 +32,20 @@ export default class Header extends Component { balance: PropTypes.object, className: PropTypes.string, children: PropTypes.node, - isContract: PropTypes.bool + isContract: PropTypes.bool, + hideName: PropTypes.bool }; static defaultProps = { className: '', children: null, - isContract: false + isContract: false, + hideName: false }; render () { const { api } = this.context; - const { account, balance, className, children } = this.props; + const { account, balance, className, children, hideName } = this.props; const { address, meta, uuid } = account; if (!account) { @@ -60,17 +62,20 @@ export default class Header extends Component {
- } /> -
+ { this.renderName(address) } + +
{ address }
+ { uuidText }
{ meta.description }
{ this.renderTxCount() }
+
@@ -89,6 +94,18 @@ export default class Header extends Component { ); } + renderName (address) { + const { hideName } = this.props; + + if (hideName) { + return null; + } + + return ( + } /> + ); + } + renderTxCount () { const { balance, isContract } = this.props; diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js index cbacd5280..840de05b9 100644 --- a/js/src/views/Account/account.js +++ b/js/src/views/Account/account.js @@ -26,7 +26,7 @@ import VerifyIcon from 'material-ui/svg-icons/action/verified-user'; import { EditMeta, DeleteAccount, Shapeshift, SMSVerification, Transfer, PasswordManager } from '~/modals'; import { Actionbar, Button, Page } from '~/ui'; -import shapeshiftBtn from '../../../assets/images/shapeshift-btn.png'; +import shapeshiftBtn from '~/../assets/images/shapeshift-btn.png'; import Header from './Header'; import Transactions from './Transactions'; diff --git a/js/src/views/Accounts/Summary/summary.js b/js/src/views/Accounts/Summary/summary.js index 3183a2903..a19b9a9de 100644 --- a/js/src/views/Accounts/Summary/summary.js +++ b/js/src/views/Accounts/Summary/summary.js @@ -153,7 +153,7 @@ export default class Summary extends Component { const { link, noLink, account, name } = this.props; const { address } = account; - const viewLink = `/${link || 'account'}/${address}`; + const viewLink = `/${link || 'accounts'}/${address}`; const content = ( diff --git a/js/src/views/Address/address.js b/js/src/views/Address/address.js index c1427b2be..9c39203ba 100644 --- a/js/src/views/Address/address.js +++ b/js/src/views/Address/address.js @@ -19,8 +19,9 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import ActionDelete from 'material-ui/svg-icons/action/delete'; import ContentCreate from 'material-ui/svg-icons/content/create'; +import ContentAdd from 'material-ui/svg-icons/content/add'; -import { EditMeta } from '~/modals'; +import { EditMeta, AddAddress } from '~/modals'; import { Actionbar, Button, Page } from '~/ui'; import Header from '../Account/Header'; @@ -32,7 +33,7 @@ class Address extends Component { static contextTypes = { api: PropTypes.object.isRequired, router: PropTypes.object.isRequired - } + }; static propTypes = { setVisibleAccounts: PropTypes.func.isRequired, @@ -40,12 +41,13 @@ class Address extends Component { contacts: PropTypes.object, balances: PropTypes.object, params: PropTypes.object - } + }; state = { showDeleteDialog: false, - showEditDialog: false - } + showEditDialog: false, + showAdd: false + }; componentDidMount () { this.setVisibleAccounts(); @@ -73,32 +75,69 @@ class Address extends Component { render () { const { contacts, balances } = this.props; const { address } = this.props.params; - const { showDeleteDialog } = this.state; + + if (Object.keys(contacts).length === 0) { + return null; + } const contact = (contacts || {})[address]; const balance = (balances || {})[address]; - if (!contact) { + return ( +
+ { this.renderAddAddress(contact, address) } + { this.renderEditDialog(contact) } + { this.renderActionbar(contact) } + { this.renderDelete(contact) } + +
+ + +
+ ); + } + + renderAddAddress (contact, address) { + if (contact) { + return null; + } + + const { contacts } = this.props; + const { showAdd } = this.state; + + if (!showAdd) { return null; } return ( -
- { this.renderEditDialog(contact) } - { this.renderActionbar(contact) } - - -
- - -
+ + ); + } + + renderDelete (contact) { + if (!contact) { + return null; + } + + const { showDeleteDialog } = this.state; + + return ( + ); } @@ -116,17 +155,27 @@ class Address extends Component { onClick={ this.showDeleteDialog } /> ]; + const addToBook = ( +
); diff --git a/js/src/views/Signer/signer.js b/js/src/views/Signer/signer.js index 6d68c1dc0..f0b4baed7 100644 --- a/js/src/views/Signer/signer.js +++ b/js/src/views/Signer/signer.js @@ -23,8 +23,7 @@ export default class Signer extends Component { render () { return (
- +
); diff --git a/js/src/views/Signer/store.js b/js/src/views/Signer/store.js index 0eeb99861..bcc85a431 100644 --- a/js/src/views/Signer/store.js +++ b/js/src/views/Signer/store.js @@ -30,12 +30,6 @@ export default class Store { } } - @action unsubscribe () { - if (this._timeoutId) { - clearTimeout(this._timeoutId); - } - } - @action setBalance = (address, balance) => { this.setBalances({ [address]: balance }); } @@ -50,6 +44,12 @@ export default class Store { } } + @action unsubscribe () { + if (this._timeoutId) { + clearTimeout(this._timeoutId); + } + } + fetchBalance (address) { this._api.eth .getBalance(address) diff --git a/js/src/views/Status/components/Debug/Debug.css b/js/src/views/Status/components/Debug/debug.css similarity index 100% rename from js/src/views/Status/components/Debug/Debug.css rename to js/src/views/Status/components/Debug/debug.css diff --git a/js/src/views/Status/components/Debug/Debug.js b/js/src/views/Status/components/Debug/debug.js similarity index 99% rename from js/src/views/Status/components/Debug/Debug.js rename to js/src/views/Status/components/Debug/debug.js index 84ce1bb87..438d208f9 100644 --- a/js/src/views/Status/components/Debug/Debug.js +++ b/js/src/views/Status/components/Debug/debug.js @@ -22,7 +22,7 @@ import ReorderIcon from 'material-ui/svg-icons/action/reorder'; import { Container } from '~/ui'; -import styles from './Debug.css'; +import styles from './debug.css'; export default class Debug extends Component { static propTypes = { diff --git a/js/src/views/Status/components/Debug/index.js b/js/src/views/Status/components/Debug/index.js index 9e8eae219..c6b522c46 100644 --- a/js/src/views/Status/components/Debug/index.js +++ b/js/src/views/Status/components/Debug/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './Debug'; +export default from './debug'; diff --git a/js/src/views/Status/components/MiningSettings/index.js b/js/src/views/Status/components/MiningSettings/index.js index 3eee1e051..7853e0d33 100644 --- a/js/src/views/Status/components/MiningSettings/index.js +++ b/js/src/views/Status/components/MiningSettings/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './MiningSettings'; +export default from './miningSettings'; diff --git a/js/src/views/Status/components/MiningSettings/MiningSettings.js b/js/src/views/Status/components/MiningSettings/miningSettings.js similarity index 100% rename from js/src/views/Status/components/MiningSettings/MiningSettings.js rename to js/src/views/Status/components/MiningSettings/miningSettings.js diff --git a/js/src/views/Status/components/Status/index.js b/js/src/views/Status/components/Status/index.js index 8885a04c6..44079b224 100644 --- a/js/src/views/Status/components/Status/index.js +++ b/js/src/views/Status/components/Status/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './Status'; +export default from './status'; diff --git a/js/src/views/Status/components/Status/Status.css b/js/src/views/Status/components/Status/status.css similarity index 90% rename from js/src/views/Status/components/Status/Status.css rename to js/src/views/Status/components/Status/status.css index d79d41062..229e72ce1 100644 --- a/js/src/views/Status/components/Status/Status.css +++ b/js/src/views/Status/components/Status/status.css @@ -28,10 +28,19 @@ content: ''; } -.blockinfo { - font-size: 24px; +.blockInfo { color: #aaa; - line-height: 24px; + font-size: 1.5em; + line-height: 1.5em; +} + +.blockByline { + color: #aaa; + font-size: 0.75em; +} + +.padBottom { + padding-bottom: 1.25em !important; } .col, diff --git a/js/src/views/Status/components/Status/Status.js b/js/src/views/Status/components/Status/status.js similarity index 84% rename from js/src/views/Status/components/Status/Status.js rename to js/src/views/Status/components/Status/status.js index e2d65cb9a..891ec6632 100644 --- a/js/src/views/Status/components/Status/Status.js +++ b/js/src/views/Status/components/Status/status.js @@ -14,14 +14,16 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import React, { Component, PropTypes } from 'react'; import bytes from 'bytes'; +import moment from 'moment'; +import React, { Component, PropTypes } from 'react'; import { Container, ContainerTitle, Input } from '~/ui'; -import styles from './Status.css'; import MiningSettings from '../MiningSettings'; +import styles from './status.css'; + export default class Status extends Component { static propTypes = { nodeStatus: PropTypes.object.isRequired, @@ -44,23 +46,26 @@ export default class Status extends Component {
-
+
-

+
#{ nodeStatus.blockNumber.toFormat() } -

+
+
+ { moment().calendar(nodeStatus.blockTimestamp) } +
-
+
-

+
{ peers } -

+
-
+
-

+
{ `${hashrate} H/s` } -

+
diff --git a/js/src/views/Status/containers/StatusPage/index.js b/js/src/views/Status/containers/StatusPage/index.js index e36bc949c..0e141a16d 100644 --- a/js/src/views/Status/containers/StatusPage/index.js +++ b/js/src/views/Status/containers/StatusPage/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './StatusPage'; +export default from './statusPage'; diff --git a/js/src/views/Status/status.css b/js/src/views/Status/containers/StatusPage/statusPage.css similarity index 93% rename from js/src/views/Status/status.css rename to js/src/views/Status/containers/StatusPage/statusPage.css index 0ed554ae6..bda975813 100644 --- a/js/src/views/Status/status.css +++ b/js/src/views/Status/containers/StatusPage/statusPage.css @@ -14,5 +14,9 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see . */ -.container { + +.body { + &>div { + margin-bottom: 0.25em; + } } diff --git a/js/src/views/Status/containers/StatusPage/StatusPage.js b/js/src/views/Status/containers/StatusPage/statusPage.js similarity index 95% rename from js/src/views/Status/containers/StatusPage/StatusPage.js rename to js/src/views/Status/containers/StatusPage/statusPage.js index c7ab56c5e..286e2c20e 100644 --- a/js/src/views/Status/containers/StatusPage/StatusPage.js +++ b/js/src/views/Status/containers/StatusPage/statusPage.js @@ -23,6 +23,8 @@ import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from '~/redux/ import Debug from '../../components/Debug'; import Status from '../../components/Status'; +import styles from './statusPage.css'; + class StatusPage extends Component { static propTypes = { nodeStatus: PropTypes.object.isRequired, @@ -39,7 +41,7 @@ class StatusPage extends Component { render () { return ( -
+
diff --git a/js/src/views/Status/status.js b/js/src/views/Status/status.js index ff13f1a07..805c96850 100644 --- a/js/src/views/Status/status.js +++ b/js/src/views/Status/status.js @@ -16,22 +16,16 @@ import React, { Component } from 'react'; -import { Actionbar, Page } from '~/ui'; +import { Page } from '~/ui'; import StatusPage from './containers/StatusPage'; -import styles from './status.css'; - export default class Status extends Component { render () { return ( -
- - - - -
+ + + ); } } diff --git a/js/test/npmLibrary.js b/js/test/npmParity.js similarity index 92% rename from js/test/npmLibrary.js rename to js/test/npmParity.js index 63d8f9515..6e125e9e2 100644 --- a/js/test/npmLibrary.js +++ b/js/test/npmParity.js @@ -15,8 +15,8 @@ // along with Parity. If not, see . try { - var Api = require('../.npmjs/library.js').Api; - var Abi = require('../.npmjs/library.js').Abi; + var Api = require('../.npmjs/parity/library.js').Api; + var Abi = require('../.npmjs/parity/library.js').Abi; if (typeof Api !== 'function') { throw new Error('No Api'); diff --git a/js/webpack/app.js b/js/webpack/app.js index 5998cf30b..a7b086b96 100644 --- a/js/webpack/app.js +++ b/js/webpack/app.js @@ -20,6 +20,7 @@ const path = require('path'); const WebpackErrorNotificationPlugin = require('webpack-error-notification'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); const Shared = require('./shared'); const DAPPS = require('../src/dapps'); @@ -41,7 +42,7 @@ module.exports = { output: { publicPath: '/', path: path.join(__dirname, '../', DEST), - filename: '[name].[hash].js' + filename: '[name].[hash:10].js' }, module: { @@ -85,13 +86,20 @@ module.exports = { { test: /\.css$/, include: [ /src/ ], + // exclude: [ /src\/dapps/ ], + loader: isProd ? ExtractTextPlugin.extract([ + // 'style-loader', + 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', + 'postcss-loader' + ]) : undefined, // use: [ 'happypack/loader?id=css' ] - use: [ + use: isProd ? undefined : [ 'style-loader', 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 'postcss-loader' ] }, + { test: /\.css$/, exclude: [ /src/ ], @@ -99,11 +107,15 @@ module.exports = { }, { test: /\.(png|jpg)$/, - use: [ 'file-loader?name=[name].[hash].[ext]' ] + use: [ 'file-loader?&name=assets/[name].[hash:10].[ext]' ] }, { - test: /\.(woff(2)|ttf|eot|svg|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/, - use: [ 'file-loader' ] + test: /\.(woff(2)|ttf|eot|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/, + use: [ 'file-loader?name=fonts/[name][hash:10].[ext]' ] + }, + { + test: /\.svg(\?v=[0-9]\.[0-9]\.[0-9])?$/, + use: [ 'file-loader?name=assets/[name].[hash:10].[ext]' ] } ], noParse: [ @@ -153,13 +165,20 @@ module.exports = { if (!isProd) { plugins.push( new webpack.optimize.CommonsChunkPlugin({ - filename: 'commons.[hash].js', + filename: 'commons.[hash:10].js', name: 'commons', minChunks: Infinity }) ); } + if (isProd) { + plugins.push(new ExtractTextPlugin({ + filename: 'styles/[name].[hash:10].css', + allChunks: true + })); + } + return plugins; }()) }; diff --git a/js/webpack/npm.js b/js/webpack/npm.js index 7353efe55..a1bbaeda9 100644 --- a/js/webpack/npm.js +++ b/js/webpack/npm.js @@ -23,14 +23,27 @@ const Shared = require('./shared'); const ENV = process.env.NODE_ENV || 'development'; const isProd = ENV === 'production'; +const LIBRARY = process.env.LIBRARY; +if (!LIBRARY) { + process.exit(-1); +} +const SRC = LIBRARY.toLowerCase(); +const OUTPUT_PATH = path.join(__dirname, '../.npmjs', SRC); + +const TEST_CONTEXT = SRC === 'parity' + ? '../npm/parity/test/' + : `../src/3rdparty/${SRC}/`; + +console.log(`Building ${LIBRARY} from library.${SRC}.js to .npmjs/${SRC}`); + module.exports = { context: path.join(__dirname, '../src'), target: 'node', - entry: 'library.js', + entry: `library.${SRC}.js`, output: { - path: path.join(__dirname, '../.npmjs'), + path: OUTPUT_PATH, filename: 'library.js', - library: 'Parity', + library: LIBRARY, libraryTarget: 'umd', umdNamedDefine: true }, @@ -66,19 +79,52 @@ module.exports = { plugins: Shared.getPlugins().concat([ new CopyWebpackPlugin([ { - from: '../parity.package.json', + from: `../npm/${SRC}/package.json`, to: 'package.json', transform: function (content, path) { const json = JSON.parse(content.toString()); json.version = packageJson.version; + + // Add tests dependencies to Dev Deps + json.devDependencies.chai = packageJson.devDependencies.chai; + json.devDependencies.mocha = packageJson.devDependencies.mocha; + json.devDependencies.nock = packageJson.devDependencies.nock; + + // Add test script + json.scripts.test = 'mocha \'test/*.spec.js\''; + return new Buffer(JSON.stringify(json, null, ' '), 'utf-8'); } }, { from: '../LICENSE' }, + + // Copy the base test config { - from: '../parity.md', + from: '../npm/test', + to: 'test' + }, + + // Copy the actual tests + { + context: TEST_CONTEXT, + from: '**/*.spec.js', + to: 'test', + transform: function (content, path) { + let output = content.toString(); + + // Don't skip tests + output = output.replace(/describe\.skip/, 'describe'); + + // Require parent library + output = output.replace('require(\'./\')', 'require(\'../\')'); + + return new Buffer(output, 'utf-8'); + } + }, + { + from: `../npm/${SRC}/README.md`, to: 'README.md' } ], { copyUnmodified: true }) diff --git a/js/webpack/shared.js b/js/webpack/shared.js index 8b6807b2a..3c593fd87 100644 --- a/js/webpack/shared.js +++ b/js/webpack/shared.js @@ -36,6 +36,29 @@ function getBabelrc () { // [ "es2015", { "modules": false } ] babelrc.presets[es2015Index] = [ 'es2015', { modules: false } ]; babelrc['babelrc'] = false; + + const BABEL_PRESET_ENV = process.env.BABEL_PRESET_ENV; + const npmStart = process.env.npm_lifecycle_event === 'start'; + const npmStartApp = process.env.npm_lifecycle_event === 'start:app'; + + if (BABEL_PRESET_ENV && (npmStart || npmStartApp)) { + console.log('using babel-preset-env'); + + babelrc.presets = [ + // 'es2017', + 'stage-0', 'react', + [ + 'env', + { + targets: { browsers: ['last 2 Chrome versions'] }, + modules: false, + loose: true, + useBuiltIns: true + } + ] + ]; + } + return babelrc; } diff --git a/parity/user_defaults.rs b/parity/user_defaults.rs index a1078b634..652abfea1 100644 --- a/parity/user_defaults.rs +++ b/parity/user_defaults.rs @@ -128,7 +128,13 @@ impl Default for UserDefaults { impl UserDefaults { pub fn load

(path: P) -> Result where P: AsRef { match File::open(path) { - Ok(file) => from_reader(file).map_err(|e| e.to_string()), + Ok(file) => match from_reader(file) { + Ok(defaults) => Ok(defaults), + Err(e) => { + warn!("Error loading user defaults file: {:?}", e); + Ok(UserDefaults::default()) + }, + }, _ => Ok(UserDefaults::default()), } } diff --git a/rpc/src/v1/impls/signer.rs b/rpc/src/v1/impls/signer.rs index 66f46ba01..f13a3d037 100644 --- a/rpc/src/v1/impls/signer.rs +++ b/rpc/src/v1/impls/signer.rs @@ -89,11 +89,13 @@ impl Signer for SignerClient where C: MiningBlockC signer.peek(&id).map(|confirmation| { let mut payload = confirmation.payload.clone(); // Modify payload - match (&mut payload, modification.gas_price) { - (&mut ConfirmationPayload::SendTransaction(ref mut request), Some(gas_price)) => { + if let ConfirmationPayload::SendTransaction(ref mut request) = payload { + if let Some(gas_price) = modification.gas_price { request.gas_price = gas_price.into(); - }, - _ => {}, + } + if let Some(gas) = modification.gas { + request.gas = gas.into(); + } } // Execute let result = dispatch::execute(&*client, &*miner, &*accounts, payload, Some(pass)); diff --git a/rpc/src/v1/tests/mocked/signer.rs b/rpc/src/v1/tests/mocked/signer.rs index ea89e5876..c87abb7eb 100644 --- a/rpc/src/v1/tests/mocked/signer.rs +++ b/rpc/src/v1/tests/mocked/signer.rs @@ -183,7 +183,7 @@ fn should_confirm_transaction_and_dispatch() { let t = Transaction { nonce: U256::zero(), gas_price: U256::from(0x1000), - gas: U256::from(10_000_000), + gas: U256::from(0x50505), action: Action::Call(recipient), value: U256::from(0x1), data: vec![] @@ -198,7 +198,7 @@ fn should_confirm_transaction_and_dispatch() { let request = r#"{ "jsonrpc":"2.0", "method":"signer_confirmRequest", - "params":["0x1", {"gasPrice":"0x1000"}, "test"], + "params":["0x1", {"gasPrice":"0x1000","gas":"0x50505"}, "test"], "id":1 }"#; let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + format!("0x{:?}", t.hash()).as_ref() + r#"","id":1}"#; diff --git a/rpc/src/v1/types/confirmations.rs b/rpc/src/v1/types/confirmations.rs index d8cfa14d6..f69018422 100644 --- a/rpc/src/v1/types/confirmations.rs +++ b/rpc/src/v1/types/confirmations.rs @@ -142,6 +142,8 @@ pub struct TransactionModification { /// Modified gas price #[serde(rename="gasPrice")] pub gas_price: Option, + /// Modified gas + pub gas: Option, } /// Represents two possible return values. @@ -275,18 +277,26 @@ mod tests { let s1 = r#"{ "gasPrice":"0xba43b7400" }"#; - let s2 = r#"{}"#; + let s2 = r#"{"gas": "0x1233"}"#; + let s3 = r#"{}"#; // when let res1: TransactionModification = serde_json::from_str(s1).unwrap(); let res2: TransactionModification = serde_json::from_str(s2).unwrap(); + let res3: TransactionModification = serde_json::from_str(s3).unwrap(); // then assert_eq!(res1, TransactionModification { gas_price: Some(U256::from_str("0ba43b7400").unwrap()), + gas: None, }); assert_eq!(res2, TransactionModification { gas_price: None, + gas: Some(U256::from_str("1233").unwrap()), + }); + assert_eq!(res3, TransactionModification { + gas_price: None, + gas: None, }); } } diff --git a/sync/src/chain.rs b/sync/src/chain.rs index ecd95c68a..876144004 100644 --- a/sync/src/chain.rs +++ b/sync/src/chain.rs @@ -1685,7 +1685,7 @@ impl ChainSync { pub fn on_packet(&mut self, io: &mut SyncIo, peer: PeerId, packet_id: u8, data: &[u8]) { if packet_id != STATUS_PACKET && !self.peers.contains_key(&peer) { - debug!(target:"sync", "Unexpected packet from unregistered peer: {}:{}", peer, io.peer_info(peer)); + debug!(target:"sync", "Unexpected packet {} from unregistered peer: {}:{}", packet_id, peer, io.peer_info(peer)); return; } let rlp = UntrustedRlp::new(data); diff --git a/sync/src/sync_io.rs b/sync/src/sync_io.rs index 8dc8c65c0..e9e2d3396 100644 --- a/sync/src/sync_io.rs +++ b/sync/src/sync_io.rs @@ -131,6 +131,10 @@ impl<'s, 'h> SyncIo for NetSyncIo<'s, 'h> { fn protocol_version(&self, protocol: &ProtocolId, peer_id: PeerId) -> u8 { self.network.protocol_version(*protocol, peer_id).unwrap_or(0) } + + fn peer_info(&self, peer_id: PeerId) -> String { + self.network.peer_client_version(peer_id) + } } diff --git a/util/network/src/host.rs b/util/network/src/host.rs index f75158e4a..975fb87b8 100644 --- a/util/network/src/host.rs +++ b/util/network/src/host.rs @@ -22,7 +22,7 @@ use std::sync::atomic::{AtomicUsize, AtomicBool, Ordering as AtomicOrdering}; use std::ops::*; use std::cmp::min; use std::path::{Path, PathBuf}; -use std::io::{Read, Write}; +use std::io::{Read, Write, ErrorKind}; use std::fs; use ethkey::{KeyPair, Secret, Random, Generator}; use mio::*; @@ -381,8 +381,6 @@ pub struct Host { impl Host { /// Create a new instance pub fn new(mut config: NetworkConfiguration, stats: Arc) -> Result { - trace!(target: "host", "Creating new Host object"); - let mut listen_address = match config.listen_address { None => SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), DEFAULT_PORT)), Some(addr) => addr, @@ -405,6 +403,7 @@ impl Host { // Setup the server socket let tcp_listener = try!(TcpListener::bind(&listen_address)); listen_address = SocketAddr::new(listen_address.ip(), try!(tcp_listener.local_addr()).port()); + debug!(target: "network", "Listening at {:?}", listen_address); let udp_port = config.udp_port.unwrap_or(listen_address.port()); let local_endpoint = NodeEndpoint { address: listen_address, udp_port: udp_port }; @@ -707,7 +706,10 @@ impl Host { } }; match TcpStream::connect(&address) { - Ok(socket) => socket, + Ok(socket) => { + trace!(target: "network", "Connecting to {:?}", address); + socket + }, Err(e) => { debug!(target: "network", "Can't connect to address {:?}: {:?}", address, e); return; @@ -749,7 +751,9 @@ impl Host { let socket = match self.tcp_listener.lock().accept() { Ok((sock, _addr)) => sock, Err(e) => { - debug!(target: "network", "Error accepting connection: {:?}", e); + if e.kind() != ErrorKind::WouldBlock { + debug!(target: "network", "Error accepting connection: {:?}", e); + } break }, }; diff --git a/util/network/src/session.rs b/util/network/src/session.rs index 9c8bed9da..3aab05d9a 100644 --- a/util/network/src/session.rs +++ b/util/network/src/session.rs @@ -435,16 +435,16 @@ impl Session { // map to protocol let protocol = self.info.capabilities[i].protocol; - let pid = packet_id - self.info.capabilities[i].id_offset; + let protocol_packet_id = packet_id - self.info.capabilities[i].id_offset; match *self.protocol_states.entry(protocol).or_insert_with(|| ProtocolState::Pending(Vec::new())) { ProtocolState::Connected => { - trace!(target: "network", "Packet {} mapped to {:?}:{}, i={}, capabilities={:?}", packet_id, protocol, pid, i, self.info.capabilities); - Ok(SessionData::Packet { data: packet.data, protocol: protocol, packet_id: pid } ) + trace!(target: "network", "Packet {} mapped to {:?}:{}, i={}, capabilities={:?}", packet_id, protocol, protocol_packet_id, i, self.info.capabilities); + Ok(SessionData::Packet { data: packet.data, protocol: protocol, packet_id: protocol_packet_id } ) } ProtocolState::Pending(ref mut pending) => { trace!(target: "network", "Packet {} deferred until protocol connection event completion", packet_id); - pending.push((packet.data, packet_id)); + pending.push((packet.data, protocol_packet_id)); Ok(SessionData::Continue) } diff --git a/util/table/src/lib.rs b/util/table/src/lib.rs index a13beab11..f65c1e171 100644 --- a/util/table/src/lib.rs +++ b/util/table/src/lib.rs @@ -18,6 +18,7 @@ use std::hash::Hash; use std::collections::HashMap; +use std::collections::hash_map::Keys; /// Structure to hold double-indexed values /// @@ -41,6 +42,11 @@ impl Table } } + /// Returns keys iterator for this Table. + pub fn keys(&self) -> Keys> { + self.map.keys() + } + /// Removes all elements from this Table pub fn clear(&mut self) { self.map.clear();