diff --git a/Cargo.lock b/Cargo.lock index 7f47eb5e7..111007321 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1293,7 +1293,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#f982c84ac216cc4f99d056c912e205bcf9341602" +source = "git+https://github.com/ethcore/js-precompiled.git#3e04f32403aab917a149d54f4191263f1c79f5ce" 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 c05e69b0f..96c217895 100644 --- a/ethcore/light/src/net/context.rs +++ b/ethcore/light/src/net/context.rs @@ -26,95 +26,95 @@ 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; } 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) + } } /// 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; - /// 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 { self.peer } - fn request_from(&self, peer: PeerId, request: Request) -> Result { - self.proto.request_from(self.io, &peer, request) - } + fn peer(&self) -> PeerId { self.peer } + 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 make_announcement(&self, announcement: Announcement) { + self.proto.make_announcement(self.io, announcement); + } - fn disconnect_peer(&self, peer: PeerId) { - self.io.disconnect_peer(peer); - } + fn disconnect_peer(&self, peer: PeerId) { + self.io.disconnect_peer(peer); + } - fn disable_peer(&self, peer: PeerId) { - self.io.disable_peer(peer); - } + fn disable_peer(&self, peer: PeerId) { + self.io.disable_peer(peer); + } } \ No newline at end of file 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 876432ce2..7c0928cdd 100644 --- a/ethcore/light/src/net/tests/mod.rs +++ b/ethcore/light/src/net/tests/mod.rs @@ -91,16 +91,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)) @@ -250,8 +261,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, @@ -284,8 +294,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, @@ -299,9 +308,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); } @@ -346,9 +355,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); } @@ -399,9 +408,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); } @@ -448,9 +457,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); } @@ -497,9 +506,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/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/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 29d7464be..577ec6f14 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.2.107", + "version": "0.2.111", "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", 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/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 (
. */ .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 c09283aec..06d4f7274 100644 --- a/js/src/ui/Page/page.css +++ b/js/src/ui/Page/page.css @@ -16,7 +16,7 @@ */ .layout { - padding: 0.25em 0.25em 1em 0.25em; + padding: 0.25em; &>div { margin-bottom: 0.75em; 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/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/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()), } }