Merge remote-tracking branch 'origin/master' into check-updates

This commit is contained in:
Gav Wood 2016-12-11 15:43:28 +01:00
commit 74a6203f72
No known key found for this signature in database
GPG Key ID: C49C1ACA1CC9B252
61 changed files with 1264 additions and 504 deletions

2
Cargo.lock generated
View File

@ -1293,7 +1293,7 @@ dependencies = [
[[package]] [[package]]
name = "parity-ui-precompiled" name = "parity-ui-precompiled"
version = "1.4.0" version = "1.4.0"
source = "git+https://github.com/ethcore/js-precompiled.git#f982c84ac216cc4f99d056c912e205bcf9341602" source = "git+https://github.com/ethcore/js-precompiled.git#3e04f32403aab917a149d54f4191263f1c79f5ce"
dependencies = [ dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]

View File

@ -14,8 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! Light client implementation. Used for raw data queries as well as the header //! Light client implementation. Stores data from light sync
//! sync.
use std::sync::Arc; use std::sync::Arc;
@ -29,7 +28,7 @@ use ethcore::transaction::SignedTransaction;
use ethcore::blockchain_info::BlockChainInfo; use ethcore::blockchain_info::BlockChainInfo;
use io::IoChannel; use io::IoChannel;
use util::hash::H256; use util::hash::{H256, H256FastMap};
use util::{Bytes, Mutex}; use util::{Bytes, Mutex};
use provider::Provider; use provider::Provider;
@ -37,9 +36,10 @@ use request;
/// Light client implementation. /// Light client implementation.
pub struct Client { pub struct Client {
engine: Arc<Engine>, _engine: Arc<Engine>,
header_queue: HeaderQueue, header_queue: HeaderQueue,
message_channel: Mutex<IoChannel<ClientIoMessage>>, _message_channel: Mutex<IoChannel<ClientIoMessage>>,
tx_pool: Mutex<H256FastMap<SignedTransaction>>,
} }
impl Client { impl Client {
@ -55,12 +55,17 @@ impl Client {
false 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. /// Fetch a vector of all pending transactions.
pub fn pending_transactions(&self) -> Vec<SignedTransaction> { pub fn pending_transactions(&self) -> Vec<SignedTransaction> {
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 { pub fn status(&self, _id: BlockId) -> BlockStatus {
BlockStatus::Unknown BlockStatus::Unknown
} }

View File

@ -28,8 +28,7 @@
//! It starts by performing a header-only sync, verifying random samples //! It starts by performing a header-only sync, verifying random samples
//! of members of the chain to varying degrees. //! of members of the chain to varying degrees.
// TODO: remove when integrating with the rest of parity. #![deny(missing_docs)]
#![allow(dead_code)]
pub mod client; pub mod client;
pub mod net; pub mod net;

View File

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

View File

@ -34,7 +34,7 @@ use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use provider::Provider; use provider::Provider;
use request::{self, Request}; use request::{self, HashOrNumber, Request};
use self::buffer_flow::{Buffer, FlowParams}; use self::buffer_flow::{Buffer, FlowParams};
use self::context::Ctx; use self::context::Ctx;
@ -57,13 +57,13 @@ const TIMEOUT_INTERVAL_MS: u64 = 1000;
// minimum interval between updates. // minimum interval between updates.
const UPDATE_INTERVAL_MS: i64 = 5000; const UPDATE_INTERVAL_MS: i64 = 5000;
// Supported protocol versions. /// Supported protocol versions.
pub const PROTOCOL_VERSIONS: &'static [u8] = &[1]; pub const PROTOCOL_VERSIONS: &'static [u8] = &[1];
// Max protocol version. /// Max protocol version.
pub const MAX_PROTOCOL_VERSION: u8 = 1; pub const MAX_PROTOCOL_VERSION: u8 = 1;
// Packet count for LES. /// Packet count for LES.
pub const PACKET_COUNT: u8 = 15; pub const PACKET_COUNT: u8 = 15;
// packet ID definitions. // packet ID definitions.
@ -102,6 +102,18 @@ mod packet {
pub const HEADER_PROOFS: u8 = 0x0e; 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. /// A request id.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ReqId(usize); pub struct ReqId(usize);
@ -111,7 +123,6 @@ pub struct ReqId(usize);
struct PendingPeer { struct PendingPeer {
sent_head: H256, sent_head: H256,
last_update: SteadyTime, last_update: SteadyTime,
proto_version: u8,
} }
// data about each peer. // data about each peer.
@ -122,7 +133,6 @@ struct Peer {
remote_flow: Option<(Buffer, FlowParams)>, remote_flow: Option<(Buffer, FlowParams)>,
sent_head: H256, // last head we've given them. sent_head: H256, // last head we've given them.
last_update: SteadyTime, last_update: SteadyTime,
proto_version: u8,
} }
impl Peer { 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 { if let Err(e) = res {
match e.punishment() { punish(*peer, io, e);
Punishment::None => {} }
Punishment::Disconnect => { }
debug!(target: "les", "Disconnecting peer {}: {}", peer, e);
io.disconnect_peer(*peer) // check timeouts and punish peers.
} fn timeout_check(&self, io: &IoContext) {
Punishment::Disable => { let now = SteadyTime::now();
debug!(target: "les", "Disabling peer {}: {}", peer, e);
io.disable_peer(*peer) // 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 { impl LightProtocol {
// called when a peer connects. // called when a peer connects.
fn on_connect(&self, peer: &PeerId, io: &IoContext) { 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); if PROTOCOL_VERSIONS.iter().find(|x| **x == proto_version).is_none() {
punish(*peer, io, Error::UnsupportedProtocolVersion(proto_version));
match self.send_status(peer, io) { return;
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);
}
} }
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. // 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<PendingPeer, Error> {
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. // Handle status message from peer.
fn status(&self, peer: &PeerId, io: &IoContext, data: UntrustedRlp) -> Result<(), Error> { fn status(&self, peer: &PeerId, io: &IoContext, data: UntrustedRlp) -> Result<(), Error> {
let pending = match self.pending_peers.write().remove(peer) { let pending = match self.pending_peers.write().remove(peer) {
@ -570,7 +603,6 @@ impl LightProtocol {
remote_flow: remote_flow, remote_flow: remote_flow,
sent_head: pending.sent_head, sent_head: pending.sent_head,
last_update: pending.last_update, last_update: pending.last_update,
proto_version: pending.proto_version,
})); }));
for handler in &self.handlers { for handler in &self.handlers {
@ -630,7 +662,7 @@ impl LightProtocol {
} }
// Handle a request for block headers. // 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; const MAX_HEADERS: usize = 512;
let peers = self.peers.read(); let peers = self.peers.read();
@ -645,18 +677,21 @@ impl LightProtocol {
let mut peer = peer.lock(); let mut peer = peer.lock();
let req_id: u64 = try!(data.val_at(0)); let req_id: u64 = try!(data.val_at(0));
let data = try!(data.at(1));
let block = { let start_block = {
let rlp = try!(data.at(1)); if try!(data.at(0)).size() == 32 {
(try!(rlp.val_at(0)), try!(rlp.val_at(1))) HashOrNumber::Hash(try!(data.val_at(0)))
} else {
HashOrNumber::Number(try!(data.val_at(0)))
}
}; };
let req = request::Headers { let req = request::Headers {
block_num: block.0, start: start_block,
block_hash: block.1, max: ::std::cmp::min(MAX_HEADERS, try!(data.val_at(1))),
max: ::std::cmp::min(MAX_HEADERS, try!(data.val_at(2))), skip: try!(data.val_at(2)),
skip: try!(data.val_at(3)), reverse: try!(data.val_at(3)),
reverse: try!(data.val_at(4)),
}; };
let max_cost = try!(peer.deduct_max(&self.flow_params, request::Kind::Headers, req.max)); 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); let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost);
io.respond(packet::BLOCK_HEADERS, { io.respond(packet::BLOCK_HEADERS, {
let mut stream = RlpStream::new_list(response.len() + 2); let mut stream = RlpStream::new_list(3);
stream.append(&req_id).append(&cur_buffer); stream.append(&req_id).append(&cur_buffer).begin_list(response.len());
for header in response { for header in response {
stream.append_raw(&header, 1); stream.append_raw(&header, 1);
@ -683,7 +718,7 @@ impl LightProtocol {
// Receive a response for block headers. // Receive a response for block headers.
fn block_headers(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> { 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 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 { for handler in &self.handlers {
handler.on_block_headers(&Ctx { handler.on_block_headers(&Ctx {
@ -713,7 +748,7 @@ impl LightProtocol {
let req_id: u64 = try!(data.val_at(0)); let req_id: u64 = try!(data.val_at(0));
let req = request::Bodies { 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())); 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); let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost);
io.respond(packet::BLOCK_BODIES, { io.respond(packet::BLOCK_BODIES, {
let mut stream = RlpStream::new_list(response.len() + 2); let mut stream = RlpStream::new_list(3);
stream.append(&req_id).append(&cur_buffer); stream.append(&req_id).append(&cur_buffer).begin_list(response.len());
for body in response { for body in response {
stream.append_raw(&body, 1); stream.append_raw(&body, 1);
@ -742,7 +777,7 @@ impl LightProtocol {
// Receive a response for block bodies. // Receive a response for block bodies.
fn block_bodies(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> { 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 req_id = try!(self.pre_verify_response(peer, request::Kind::Bodies, &raw));
let raw_bodies: Vec<Bytes> = raw.iter().skip(2).map(|x| x.as_raw().to_owned()).collect(); let raw_bodies: Vec<Bytes> = try!(raw.at(2)).iter().map(|x| x.as_raw().to_owned()).collect();
for handler in &self.handlers { for handler in &self.handlers {
handler.on_block_bodies(&Ctx { handler.on_block_bodies(&Ctx {
@ -772,7 +807,7 @@ impl LightProtocol {
let req_id: u64 = try!(data.val_at(0)); let req_id: u64 = try!(data.val_at(0));
let req = request::Receipts { 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())); 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); let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost);
io.respond(packet::RECEIPTS, { io.respond(packet::RECEIPTS, {
let mut stream = RlpStream::new_list(response.len() + 2); let mut stream = RlpStream::new_list(3);
stream.append(&req_id).append(&cur_buffer); stream.append(&req_id).append(&cur_buffer).begin_list(response.len());
for receipts in response { for receipts in response {
stream.append_raw(&receipts, 1); stream.append_raw(&receipts, 1);
@ -801,9 +836,8 @@ impl LightProtocol {
// Receive a response for receipts. // Receive a response for receipts.
fn receipts(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> { 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 req_id = try!(self.pre_verify_response(peer, request::Kind::Receipts, &raw));
let raw_receipts: Vec<Vec<Receipt>> = try!(raw let raw_receipts: Vec<Vec<Receipt>> = try!(try!(raw.at(2))
.iter() .iter()
.skip(2)
.map(|x| x.as_val()) .map(|x| x.as_val())
.collect()); .collect());
@ -835,7 +869,7 @@ impl LightProtocol {
let req_id: u64 = try!(data.val_at(0)); let req_id: u64 = try!(data.val_at(0));
let req = { let req = {
let requests: Result<Vec<_>, Error> = data.iter().skip(1).take(MAX_PROOFS).map(|x| { let requests: Result<Vec<_>, Error> = try!(data.at(1)).iter().take(MAX_PROOFS).map(|x| {
Ok(request::StateProof { Ok(request::StateProof {
block: try!(x.val_at(0)), block: try!(x.val_at(0)),
key1: try!(x.val_at(1)), key1: try!(x.val_at(1)),
@ -859,8 +893,8 @@ impl LightProtocol {
let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost); let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost);
io.respond(packet::PROOFS, { io.respond(packet::PROOFS, {
let mut stream = RlpStream::new_list(response.len() + 2); let mut stream = RlpStream::new_list(3);
stream.append(&req_id).append(&cur_buffer); stream.append(&req_id).append(&cur_buffer).begin_list(response.len());
for proof in response { for proof in response {
stream.append_raw(&proof, 1); stream.append_raw(&proof, 1);
@ -876,8 +910,7 @@ impl LightProtocol {
fn proofs(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> { 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 req_id = try!(self.pre_verify_response(peer, request::Kind::StateProofs, &raw));
let raw_proofs: Vec<Vec<Bytes>> = raw.iter() let raw_proofs: Vec<Vec<Bytes>> = try!(raw.at(2)).iter()
.skip(2)
.map(|x| x.iter().map(|node| node.as_raw().to_owned()).collect()) .map(|x| x.iter().map(|node| node.as_raw().to_owned()).collect())
.collect(); .collect();
@ -909,7 +942,7 @@ impl LightProtocol {
let req_id: u64 = try!(data.val_at(0)); let req_id: u64 = try!(data.val_at(0));
let req = { let req = {
let requests: Result<Vec<_>, Error> = data.iter().skip(1).take(MAX_CODES).map(|x| { let requests: Result<Vec<_>, Error> = try!(data.at(1)).iter().take(MAX_CODES).map(|x| {
Ok(request::ContractCode { Ok(request::ContractCode {
block_hash: try!(x.val_at(0)), block_hash: try!(x.val_at(0)),
account_key: try!(x.val_at(1)), 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); let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost);
io.respond(packet::CONTRACT_CODES, { io.respond(packet::CONTRACT_CODES, {
let mut stream = RlpStream::new_list(response.len() + 2); let mut stream = RlpStream::new_list(3);
stream.append(&req_id).append(&cur_buffer); stream.append(&req_id).append(&cur_buffer).begin_list(response.len());
for code in response { for code in response {
stream.append(&code); stream.append(&code);
@ -948,7 +981,7 @@ impl LightProtocol {
fn contract_code(&self, peer: &PeerId, io: &IoContext, raw: UntrustedRlp) -> Result<(), Error> { 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 req_id = try!(self.pre_verify_response(peer, request::Kind::Codes, &raw));
let raw_code: Vec<Bytes> = try!(raw.iter().skip(2).map(|x| x.as_val()).collect()); let raw_code: Vec<Bytes> = try!(try!(raw.at(2)).iter().map(|x| x.as_val()).collect());
for handler in &self.handlers { for handler in &self.handlers {
handler.on_code(&Ctx { handler.on_code(&Ctx {
@ -978,7 +1011,7 @@ impl LightProtocol {
let req_id: u64 = try!(data.val_at(0)); let req_id: u64 = try!(data.val_at(0));
let req = { let req = {
let requests: Result<Vec<_>, Error> = data.iter().skip(1).take(MAX_PROOFS).map(|x| { let requests: Result<Vec<_>, Error> = try!(data.at(1)).iter().take(MAX_PROOFS).map(|x| {
Ok(request::HeaderProof { Ok(request::HeaderProof {
cht_number: try!(x.val_at(0)), cht_number: try!(x.val_at(0)),
block_number: try!(x.val_at(1)), 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); let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost);
io.respond(packet::HEADER_PROOFS, { io.respond(packet::HEADER_PROOFS, {
let mut stream = RlpStream::new_list(response.len() + 2); let mut stream = RlpStream::new_list(3);
stream.append(&req_id).append(&cur_buffer); stream.append(&req_id).append(&cur_buffer).begin_list(response.len());
for proof in response { for proof in response {
stream.append_raw(&proof, 1); 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 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 { for handler in &self.handlers {
handler.on_header_proofs(&Ctx { 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 { impl NetworkProtocolHandler for LightProtocol {
fn initialize(&self, io: &NetworkContext) { fn initialize(&self, io: &NetworkContext) {
io.register_timer(TIMEOUT, TIMEOUT_INTERVAL_MS).expect("Error registering sync timer."); 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); self.on_disconnect(*peer, io);
} }
fn timeout(&self, _io: &NetworkContext, timer: TimerToken) { fn timeout(&self, io: &NetworkContext, timer: TimerToken) {
match timer { match timer {
TIMEOUT => { TIMEOUT => self.timeout_check(io),
// broadcast transactions to peers.
}
_ => warn!(target: "les", "received timeout on unknown token {}", timer), _ => 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<u8> { fn encode_request(req: &Request, req_id: usize) -> Vec<u8> {
match *req { match *req {
Request::Headers(ref headers) => { 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 stream
.append(&req_id)
.begin_list(2)
.append(&headers.block_num)
.append(&headers.block_hash)
.append(&headers.max) .append(&headers.max)
.append(&headers.skip) .append(&headers.skip)
.append(&headers.reverse); .append(&headers.reverse);
stream.out() stream.out()
} }
Request::Bodies(ref request) => { Request::Bodies(ref request) => {
let mut stream = RlpStream::new_list(request.block_hashes.len() + 1); let mut stream = RlpStream::new_list(2);
stream.append(&req_id); stream.append(&req_id).begin_list(request.block_hashes.len());
for hash in &request.block_hashes { for hash in &request.block_hashes {
stream.append(hash); stream.append(hash);
@ -1111,8 +1160,8 @@ fn encode_request(req: &Request, req_id: usize) -> Vec<u8> {
stream.out() stream.out()
} }
Request::Receipts(ref request) => { Request::Receipts(ref request) => {
let mut stream = RlpStream::new_list(request.block_hashes.len() + 1); let mut stream = RlpStream::new_list(2);
stream.append(&req_id); stream.append(&req_id).begin_list(request.block_hashes.len());
for hash in &request.block_hashes { for hash in &request.block_hashes {
stream.append(hash); stream.append(hash);
@ -1121,8 +1170,8 @@ fn encode_request(req: &Request, req_id: usize) -> Vec<u8> {
stream.out() stream.out()
} }
Request::StateProofs(ref request) => { Request::StateProofs(ref request) => {
let mut stream = RlpStream::new_list(request.requests.len() + 1); let mut stream = RlpStream::new_list(2);
stream.append(&req_id); stream.append(&req_id).begin_list(request.requests.len());
for proof_req in &request.requests { for proof_req in &request.requests {
stream.begin_list(4) stream.begin_list(4)
@ -1140,8 +1189,8 @@ fn encode_request(req: &Request, req_id: usize) -> Vec<u8> {
stream.out() stream.out()
} }
Request::Codes(ref request) => { Request::Codes(ref request) => {
let mut stream = RlpStream::new_list(request.code_requests.len() + 1); let mut stream = RlpStream::new_list(2);
stream.append(&req_id); stream.append(&req_id).begin_list(request.code_requests.len());
for code_req in &request.code_requests { for code_req in &request.code_requests {
stream.begin_list(2) stream.begin_list(2)
@ -1152,8 +1201,8 @@ fn encode_request(req: &Request, req_id: usize) -> Vec<u8> {
stream.out() stream.out()
} }
Request::HeaderProofs(ref request) => { Request::HeaderProofs(ref request) => {
let mut stream = RlpStream::new_list(request.requests.len() + 1); let mut stream = RlpStream::new_list(2);
stream.append(&req_id); stream.append(&req_id).begin_list(request.requests.len());
for proof_req in &request.requests { for proof_req in &request.requests {
stream.begin_list(3) stream.begin_list(3)

View File

@ -91,16 +91,27 @@ impl Provider for TestProvider {
} }
fn block_headers(&self, req: request::Headers) -> Vec<Bytes> { fn block_headers(&self, req: request::Headers) -> Vec<Bytes> {
let best_num = self.0.client.chain_info().best_block_number; use request::HashOrNumber;
let start_num = req.block_num; use ethcore::views::HeaderView;
match self.0.client.block_hash(BlockId::Number(req.block_num)) { let best_num = self.chain_info().best_block_number;
Some(hash) if hash == req.block_hash => {} let start_num = match req.start {
_=> { HashOrNumber::Number(start_num) => start_num,
trace!(target: "les_provider", "unknown/non-canonical start block in header request: {:?}", (req.block_num, req.block_hash)); HashOrNumber::Hash(hash) => match self.0.client.block_header(BlockId::Hash(hash)) {
return vec![] 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) (0u64..req.max as u64)
.map(|x: u64| x.saturating_mul(req.skip + 1)) .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. // 1000 requests is far too many for the default flow params.
let request = encode_request(&Request::Headers(Headers { let request = encode_request(&Request::Headers(Headers {
block_num: 1, start: 1.into(),
block_hash: provider.client.chain_info().genesis_hash,
max: 1000, max: 1000,
skip: 0, skip: 0,
reverse: false, reverse: false,
@ -284,8 +294,7 @@ fn get_block_headers() {
} }
let request = Headers { let request = Headers {
block_num: 1, start: 1.into(),
block_hash: provider.client.block_hash(BlockId::Number(1)).unwrap(),
max: 10, max: 10,
skip: 0, skip: 0,
reverse: false, 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 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 { for header in headers {
response_stream.append_raw(&header, 1); 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 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 { for body in bodies {
response_stream.append_raw(&body, 1); 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 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 { for block_receipts in receipts {
response_stream.append_raw(&block_receipts, 1); 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 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 { for proof in proofs {
response_stream.append_raw(&proof, 1); 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 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 { for code in codes {
response_stream.append(&code); response_stream.append(&code);
} }

View File

@ -97,17 +97,29 @@ impl<T: ProvingBlockChainClient + ?Sized> Provider for T {
} }
fn block_headers(&self, req: request::Headers) -> Vec<Bytes> { fn block_headers(&self, req: request::Headers) -> Vec<Bytes> {
use request::HashOrNumber;
use ethcore::views::HeaderView;
let best_num = self.chain_info().best_block_number; 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)) { num
Some(hash) if hash == req.block_hash => {} }
_=> {
trace!(target: "les_provider", "unknown/non-canonical start block in header request: {:?}", (req.block_num, req.block_hash));
return vec![]
} }
} };
(0u64..req.max as u64) (0u64..req.max as u64)
.map(|x: u64| x.saturating_mul(req.skip + 1)) .map(|x: u64| x.saturating_mul(req.skip + 1))
.take_while(|x| if req.reverse { x < &start_num } else { best_num - start_num >= *x }) .take_while(|x| if req.reverse { x < &start_num } else { best_num - start_num >= *x })

View File

@ -18,15 +18,34 @@
use util::H256; 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<H256> for HashOrNumber {
fn from(hash: H256) -> Self {
HashOrNumber::Hash(hash)
}
}
impl From<u64> for HashOrNumber {
fn from(num: u64) -> Self {
HashOrNumber::Number(num)
}
}
/// A request for block headers. /// A request for block headers.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "ipc", derive(Binary))] #[cfg_attr(feature = "ipc", derive(Binary))]
pub struct Headers { pub struct Headers {
/// Starting block number /// Starting block number or hash.
pub block_num: u64, pub start: HashOrNumber,
/// Starting block hash. This and number could be combined but IPC codegen is
/// not robust enough to support it.
pub block_hash: H256,
/// The maximum amount of headers which can be returned. /// The maximum amount of headers which can be returned.
pub max: usize, pub max: usize,
/// The amount of headers to skip between each response entry. /// The amount of headers to skip between each response entry.

View File

@ -266,7 +266,8 @@ impl<'x> OpenBlock<'x> {
r.block.base.header.set_extra_data(extra_data); r.block.base.header.set_extra_data(extra_data);
r.block.base.header.note_dirty(); 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); engine.on_new_block(&mut r.block);
Ok(r) Ok(r)
} }

View File

@ -125,8 +125,9 @@ pub trait Engine : Sync + Send {
self.verify_block_basic(header, None).and_then(|_| self.verify_block_unordered(header, None)) 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. /// Populate a header's fields based on its parent's header.
// TODO: consider including State in the params. /// 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) { 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_difficulty(parent.difficulty().clone());
header.set_gas_limit(parent.gas_limit().clone()); header.set_gas_limit(parent.gas_limit().clone());

View File

@ -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()`

View File

@ -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 <admin@parity.io>",
"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"
}
}

View File

@ -1,7 +1,9 @@
# parity.js # @parity/parity.js
Parity.js is a thin, fast, Promise-based wrapper around the Ethereum APIs. 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 ## installation
Install the package with `npm install --save @parity/parity.js` Install the package with `npm install --save @parity/parity.js`

View File

@ -1,6 +1,6 @@
{ {
"name": "@parity/parity.js", "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", "version": "0.0.0",
"main": "library.js", "main": "library.js",
"author": "Parity Team <admin@parity.io>", "author": "Parity Team <admin@parity.io>",
@ -26,8 +26,8 @@
"devDependencies": { "devDependencies": {
}, },
"dependencies": { "dependencies": {
"bignumber.js": "^2.3.0", "bignumber.js": "~2.3.0",
"js-sha3": "^0.5.2", "js-sha3": "~0.5.2",
"node-fetch": "^1.6.3" "node-fetch": "~1.6.3"
} }
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
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;
});
});

View File

@ -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)

View File

@ -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 <admin@parity.io>",
"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"
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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 = {};

1
js/npm/test/mocha.opts Normal file
View File

@ -0,0 +1 @@
-r ./test/mocha.config

View File

@ -1,6 +1,6 @@
{ {
"name": "parity.js", "name": "parity.js",
"version": "0.2.107", "version": "0.2.111",
"main": "release/index.js", "main": "release/index.js",
"jsnext:main": "src/index.js", "jsnext:main": "src/index.js",
"author": "Parity Team <admin@parity.io>", "author": "Parity Team <admin@parity.io>",
@ -43,7 +43,7 @@
"test": "NODE_ENV=test mocha 'src/**/*.spec.js'", "test": "NODE_ENV=test mocha 'src/**/*.spec.js'",
"test:coverage": "NODE_ENV=test istanbul cover _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: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" "prepush": "npm run lint:cached"
}, },
"devDependencies": { "devDependencies": {
@ -59,6 +59,7 @@
"babel-plugin-transform-runtime": "6.15.0", "babel-plugin-transform-runtime": "6.15.0",
"babel-plugin-webpack-alias": "2.1.2", "babel-plugin-webpack-alias": "2.1.2",
"babel-polyfill": "6.20.0", "babel-polyfill": "6.20.0",
"babel-preset-env": "1.0.2",
"babel-preset-es2015": "6.18.0", "babel-preset-es2015": "6.18.0",
"babel-preset-es2016": "6.16.0", "babel-preset-es2016": "6.16.0",
"babel-preset-es2017": "6.16.0", "babel-preset-es2017": "6.16.0",

33
js/scripts/dryrun-npm.sh Executable file
View File

@ -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

View File

@ -3,7 +3,7 @@ set -e
# variables # variables
UTCDATE=`date -u "+%Y%m%d-%H%M%S"` UTCDATE=`date -u "+%Y%m%d-%H%M%S"`
PACKAGES=( "parity.js" ) PACKAGES=( "parity" "etherscan" "shapeshift" )
BRANCH=$CI_BUILD_REF_NAME BRANCH=$CI_BUILD_REF_NAME
GIT_JS_PRECOMPILED="https://${GITHUB_JS_PRECOMPILED}:@github.com/ethcore/js-precompiled.git" GIT_JS_PRECOMPILED="https://${GITHUB_JS_PRECOMPILED}:@github.com/ethcore/js-precompiled.git"
GIT_PARITY="https://${GITHUB_JS_PRECOMPILED}:@github.com/ethcore/parity.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 if [ "$BRANCH" == "master" ]; then
cd js cd js
echo "*** Bumping package.json patch version" echo "*** Bumping package.json patch version"
npm --no-git-tag-version version npm --no-git-tag-version version
npm version patch npm version patch
echo "*** Building packages for npmjs" echo "*** Building packages for npmjs"
# echo -e "$NPM_USERNAME\n$NPM_PASSWORD\n$NPM_EMAIL" | npm login
echo "$NPM_TOKEN" >> ~/.npmrc echo "$NPM_TOKEN" >> ~/.npmrc
npm run ci:build:npm
echo "*** Publishing $PACKAGE to npmjs" for PACKAGE in ${PACKAGES[@]}
cd .npmjs do
npm publish --access public || true echo "*** Building $PACKAGE"
cd ../.. 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 fi
echo "*** Updating cargo parity-ui-precompiled#$PRECOMPILED_HASH" echo "*** Updating cargo parity-ui-precompiled#$PRECOMPILED_HASH"

View File

@ -14,11 +14,13 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import etherscan from './'; const etherscan = require('./');
const TESTADDR = '0xbf885e2b55c6bcc84556a3c5f07d3040833c8d00'; const TESTADDR = '0xbf885e2b55c6bcc84556a3c5f07d3040833c8d00';
describe.skip('etherscan/account', () => { describe.skip('etherscan/account', function () {
this.timeout(60 * 1000);
const checkBalance = function (balance, addr) { const checkBalance = function (balance, addr) {
expect(balance).to.be.ok; expect(balance).to.be.ok;
expect(balance.account).to.equal(addr); expect(balance.account).to.equal(addr);

View File

@ -14,9 +14,11 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import etherscan from './'; const etherscan = require('./');
describe.skip('etherscan/stats', function () {
this.timeout(60 * 1000);
describe.skip('etherscan/stats', () => {
it('retrieves the latest price', () => { it('retrieves the latest price', () => {
return etherscan.stats return etherscan.stats
.price() .price()

View File

@ -14,22 +14,15 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import chai from 'chai'; const nock = require('nock');
import nock from 'nock';
global.expect = chai.expect; // eslint-disable-line no-undef const ShapeShift = require('./');
const initShapeshift = (ShapeShift.default || ShapeShift);
import 'isomorphic-fetch';
import es6Promise from 'es6-promise';
es6Promise.polyfill();
import initShapeshift from './';
import initRpc from './rpc';
const APIKEY = '0x123454321'; const APIKEY = '0x123454321';
const shapeshift = initShapeshift(APIKEY); const shapeshift = initShapeshift(APIKEY);
const rpc = initRpc(APIKEY); const rpc = shapeshift.getRpc();
function mockget (requests) { function mockget (requests) {
let scope = nock(rpc.ENDPOINT); let scope = nock(rpc.ENDPOINT);
@ -62,7 +55,7 @@ function mockpost (requests) {
return scope; return scope;
} }
export { module.exports = {
APIKEY, APIKEY,
mockget, mockget,
mockpost, mockpost,

View File

@ -14,7 +14,12 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { 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('shapeshift/rpc', () => {
describe('GET', () => { describe('GET', () => {

View File

@ -26,6 +26,10 @@ export default function (rpc) {
return rpc.get(`marketinfo/${pair}`); return rpc.get(`marketinfo/${pair}`);
} }
function getRpc () {
return rpc;
}
function getStatus (depositAddress) { function getStatus (depositAddress) {
return rpc.get(`txStat/${depositAddress}`); return rpc.get(`txStat/${depositAddress}`);
} }
@ -103,6 +107,7 @@ export default function (rpc) {
return { return {
getCoins, getCoins,
getMarketInfo, getMarketInfo,
getRpc,
getStatus, getStatus,
shift, shift,
subscribe, subscribe,

View File

@ -14,7 +14,11 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { 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('shapeshift/calls', () => {
describe('getCoins', () => { describe('getCoins', () => {

View File

@ -62,6 +62,10 @@ export default class Contracts {
} }
static create (api) { static create (api) {
if (instance) {
return instance;
}
return new Contracts(api); return new Contracts(api);
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
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;

View File

@ -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 <http://www.gnu.org/licenses/>.
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;

View File

@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { 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'; 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 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 () { render () {
const { routerHistory } = this.props; const { routerHistory } = this.props;
@ -34,26 +51,46 @@ export default class MainApplication extends Component {
<Redirect from='/' to='/accounts' /> <Redirect from='/' to='/accounts' />
<Redirect from='/auth' to='/accounts' query={ {} } /> <Redirect from='/auth' to='/accounts' query={ {} } />
<Redirect from='/settings' to='/settings/views' /> <Redirect from='/settings' to='/settings/views' />
{ /** Backward Compatible links */ }
<Route path='/account/:address' onEnter={ this.handleDeprecatedRoute } />
<Route path='/address/:address' onEnter={ this.handleDeprecatedRoute } />
<Route path='/contract/:address' onEnter={ this.handleDeprecatedRoute } />
<Route path='/' component={ Application }> <Route path='/' component={ Application }>
<Route path='accounts' component={ Accounts } /> <Route path='accounts'>
<Route path='account/:address' component={ Account } /> <IndexRoute component={ Accounts } />
<Route path='wallet/:address' component={ Wallet } /> <Route path=':address' component={ Account } />
<Route path='addresses' component={ Addresses } /> <Route path='/wallet/:address' component={ Wallet } />
<Route path='address/:address' component={ Address } /> </Route>
<Route path='addresses'>
<IndexRoute component={ Addresses } />
<Route path=':address' component={ Address } />
</Route>
<Route path='apps' component={ Dapps } /> <Route path='apps' component={ Dapps } />
<Route path='app/:id' component={ Dapp } /> <Route path='app/:id' component={ Dapp } />
<Route path='contracts' component={ Contracts } />
<Route path='contracts/write' component={ WriteContract } /> <Route path='contracts'>
<Route path='contract/:address' component={ Contract } /> <IndexRoute component={ Contracts } />
<Route path='develop' component={ WriteContract } />
<Route path=':address' component={ Contract } />
</Route>
<Route path='settings' component={ Settings }> <Route path='settings' component={ Settings }>
<Route path='background' component={ SettingsBackground } /> <Route path='background' component={ SettingsBackground } />
<Route path='proxy' component={ SettingsProxy } /> <Route path='proxy' component={ SettingsProxy } />
<Route path='views' component={ SettingsViews } /> <Route path='views' component={ SettingsViews } />
<Route path='parity' component={ SettingsParity } /> <Route path='parity' component={ SettingsParity } />
</Route> </Route>
<Route path='signer' component={ Signer } /> <Route path='signer' component={ Signer } />
<Route path='status' component={ Status } />
<Route path='status/:subpage' component={ Status } /> <Route path='status'>
<IndexRoute component={ Status } />
<Route path=':subpage' component={ Status } />
</Route>
</Route> </Route>
</Router> </Router>
); );

View File

@ -28,6 +28,7 @@ export default class AddAddress extends Component {
static propTypes = { static propTypes = {
contacts: PropTypes.object.isRequired, contacts: PropTypes.object.isRequired,
address: PropTypes.string,
onClose: PropTypes.func onClose: PropTypes.func
}; };
@ -39,6 +40,12 @@ export default class AddAddress extends Component {
description: '' description: ''
}; };
componentWillMount () {
if (this.props.address) {
this.onEditAddress(null, this.props.address);
}
}
render () { render () {
return ( return (
<Modal <Modal
@ -77,6 +84,8 @@ export default class AddAddress extends Component {
hint='the network address for the entry' hint='the network address for the entry'
error={ addressError } error={ addressError }
value={ address } value={ address }
disabled={ !!this.props.address }
allowCopy={ false }
onChange={ this.onEditAddress } /> onChange={ this.onEditAddress } />
<Input <Input
label='address name' label='address name'

View File

@ -30,10 +30,12 @@ export default class WalletInfo extends Component {
owners: PropTypes.array.isRequired, owners: PropTypes.array.isRequired,
required: PropTypes.oneOfType([ required: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
PropTypes.object,
PropTypes.number PropTypes.number
]).isRequired, ]).isRequired,
daylimit: PropTypes.oneOfType([ daylimit: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
PropTypes.object,
PropTypes.number PropTypes.number
]).isRequired, ]).isRequired,

View File

@ -28,26 +28,25 @@ export default class DetailsStep extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
onFromAddressChange: PropTypes.func.isRequired,
onNameChange: PropTypes.func.isRequired,
onDescriptionChange: PropTypes.func.isRequired,
onAbiChange: PropTypes.func.isRequired, onAbiChange: PropTypes.func.isRequired,
onCodeChange: PropTypes.func.isRequired, onCodeChange: PropTypes.func.isRequired,
onParamsChange: PropTypes.func.isRequired, onDescriptionChange: PropTypes.func.isRequired,
onFromAddressChange: PropTypes.func.isRequired,
onInputsChange: PropTypes.func.isRequired, onInputsChange: PropTypes.func.isRequired,
onNameChange: PropTypes.func.isRequired,
onParamsChange: PropTypes.func.isRequired,
abi: PropTypes.string,
abiError: PropTypes.string,
balances: PropTypes.object,
code: PropTypes.string,
codeError: PropTypes.string,
description: PropTypes.string,
descriptionError: PropTypes.string,
fromAddress: PropTypes.string, fromAddress: PropTypes.string,
fromAddressError: PropTypes.string, fromAddressError: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
nameError: PropTypes.string, nameError: PropTypes.string,
description: PropTypes.string,
descriptionError: PropTypes.string,
abi: PropTypes.string,
abiError: PropTypes.string,
code: PropTypes.string,
codeError: PropTypes.string,
readOnly: PropTypes.bool readOnly: PropTypes.bool
}; };
@ -77,6 +76,7 @@ export default class DetailsStep extends Component {
render () { render () {
const { const {
accounts, accounts,
balances,
readOnly, readOnly,
fromAddress, fromAddressError, fromAddress, fromAddressError,
@ -94,24 +94,28 @@ export default class DetailsStep extends Component {
<AddressSelect <AddressSelect
label='from account (contract owner)' label='from account (contract owner)'
hint='the owner account for this contract' hint='the owner account for this contract'
value={ fromAddress }
error={ fromAddressError }
accounts={ accounts } accounts={ accounts }
onChange={ this.onFromAddressChange } /> balances={ balances }
error={ fromAddressError }
onChange={ this.onFromAddressChange }
value={ fromAddress }
/>
<Input <Input
label='contract name' label='contract name'
hint='a name for the deployed contract' hint='a name for the deployed contract'
error={ nameError } error={ nameError }
onChange={ this.onNameChange }
value={ name || '' } value={ name || '' }
onChange={ this.onNameChange } /> />
<Input <Input
label='contract description (optional)' label='contract description (optional)'
hint='a description for the contract' hint='a description for the contract'
error={ descriptionError } error={ descriptionError }
onChange={ this.onDescriptionChange }
value={ description } value={ description }
onChange={ this.onDescriptionChange } /> />
{ this.renderContractSelect() } { this.renderContractSelect() }
@ -119,17 +123,19 @@ export default class DetailsStep extends Component {
label='abi / solc combined-output' label='abi / solc combined-output'
hint='the abi of the contract to deploy or solc combined-output' hint='the abi of the contract to deploy or solc combined-output'
error={ abiError } error={ abiError }
value={ solcOutput }
onChange={ this.onSolcChange } onChange={ this.onSolcChange }
onSubmit={ this.onSolcSubmit } onSubmit={ this.onSolcSubmit }
readOnly={ readOnly } /> readOnly={ readOnly }
value={ solcOutput }
/>
<Input <Input
label='code' label='code'
hint='the compiled code of the contract to deploy' hint='the compiled code of the contract to deploy'
error={ codeError } error={ codeError }
value={ code }
onSubmit={ this.onCodeChange } onSubmit={ this.onCodeChange }
readOnly={ readOnly || solc } /> readOnly={ readOnly || solc }
value={ code }
/>
</Form> </Form>
); );

View File

@ -15,8 +15,10 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import { pick } from 'lodash';
import { BusyStep, CompletedStep, CopyToClipboard, Button, IdentityIcon, Modal, TxHash } from '~/ui'; import { BusyStep, CompletedStep, CopyToClipboard, Button, IdentityIcon, Modal, TxHash } from '~/ui';
import { ERRORS, validateAbi, validateCode, validateName } from '~/util/validation'; import { ERRORS, validateAbi, validateCode, validateName } from '~/util/validation';
@ -36,7 +38,7 @@ const STEPS = {
COMPLETED: { title: 'completed' } COMPLETED: { title: 'completed' }
}; };
export default class DeployContract extends Component { class DeployContract extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired, api: PropTypes.object.isRequired,
store: PropTypes.object.isRequired store: PropTypes.object.isRequired
@ -45,6 +47,7 @@ export default class DeployContract extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
balances: PropTypes.object,
abi: PropTypes.string, abi: PropTypes.string,
code: PropTypes.string, code: PropTypes.string,
readOnly: PropTypes.bool, readOnly: PropTypes.bool,
@ -192,7 +195,7 @@ export default class DeployContract extends Component {
} }
renderStep () { renderStep () {
const { accounts, readOnly } = this.props; const { accounts, readOnly, balances } = this.props;
const { address, deployError, step, deployState, txhash, rejected } = this.state; const { address, deployError, step, deployState, txhash, rejected } = this.state;
if (deployError) { if (deployError) {
@ -216,6 +219,7 @@ export default class DeployContract extends Component {
<DetailsStep <DetailsStep
{ ...this.state } { ...this.state }
accounts={ accounts } accounts={ accounts }
balances={ balances }
readOnly={ readOnly } readOnly={ readOnly }
onFromAddressChange={ this.onFromAddressChange } onFromAddressChange={ this.onFromAddressChange }
onDescriptionChange={ this.onDescriptionChange } onDescriptionChange={ this.onDescriptionChange }
@ -394,3 +398,17 @@ export default class DeployContract extends Component {
this.props.onClose(); this.props.onClose();
} }
} }
function mapStateToProps (initState, initProps) {
const fromAddresses = Object.keys(initProps.accounts);
return (state) => {
const balances = pick(state.balances.balances, fromAddresses);
return { balances };
};
}
export default connect(
mapStateToProps
)(DeployContract);

View File

@ -32,25 +32,27 @@ export default class DetailsStep extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
contract: PropTypes.object.isRequired, contract: PropTypes.object.isRequired,
amount: PropTypes.string,
amountError: PropTypes.string,
onAmountChange: PropTypes.func.isRequired, onAmountChange: PropTypes.func.isRequired,
fromAddress: PropTypes.string,
fromAddressError: PropTypes.string,
gasEdit: PropTypes.bool,
onFromAddressChange: PropTypes.func.isRequired, onFromAddressChange: PropTypes.func.isRequired,
func: PropTypes.object, onValueChange: PropTypes.func.isRequired,
funcError: PropTypes.string,
onFuncChange: PropTypes.func,
onGasEditClick: PropTypes.func,
values: PropTypes.array.isRequired, values: PropTypes.array.isRequired,
valuesError: PropTypes.array.isRequired, valuesError: PropTypes.array.isRequired,
warning: PropTypes.string,
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 () { 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 ( return (
<Form> <Form>
@ -61,6 +63,7 @@ export default class DetailsStep extends Component {
value={ fromAddress } value={ fromAddress }
error={ fromAddressError } error={ fromAddressError }
accounts={ accounts } accounts={ accounts }
balances={ balances }
onChange={ onFromAddressChange } /> onChange={ onFromAddressChange } />
{ this.renderFunctionSelect() } { this.renderFunctionSelect() }
{ this.renderParameters() } { this.renderParameters() }

View File

@ -18,6 +18,8 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { pick } from 'lodash';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back'; import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
@ -57,6 +59,7 @@ class ExecuteContract extends Component {
isTest: PropTypes.bool, isTest: PropTypes.bool,
fromAddress: PropTypes.string, fromAddress: PropTypes.string,
accounts: PropTypes.object, accounts: PropTypes.object,
balances: PropTypes.object,
contract: PropTypes.object, contract: PropTypes.object,
gasLimit: PropTypes.object.isRequired, gasLimit: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
@ -362,10 +365,15 @@ class ExecuteContract extends Component {
} }
} }
function mapStateToProps (state) { function mapStateToProps (initState, initProps) {
const { gasLimit } = state.nodeStatus; 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) { function mapDispatchToProps (dispatch) {

View File

@ -134,6 +134,7 @@ export default class Details extends Component {
images: PropTypes.object.isRequired, images: PropTypes.object.isRequired,
sender: PropTypes.string, sender: PropTypes.string,
senderError: PropTypes.string, senderError: PropTypes.string,
sendersBalances: PropTypes.object,
recipient: PropTypes.string, recipient: PropTypes.string,
recipientError: PropTypes.string, recipientError: PropTypes.string,
tag: PropTypes.string, tag: PropTypes.string,
@ -203,7 +204,7 @@ export default class Details extends Component {
} }
renderFromAddress () { renderFromAddress () {
const { sender, senderError, senders } = this.props; const { sender, senderError, senders, sendersBalances } = this.props;
if (!senders) { if (!senders) {
return null; return null;
@ -218,6 +219,7 @@ export default class Details extends Component {
hint='the sender address' hint='the sender address'
value={ sender } value={ sender }
onChange={ this.onEditSender } onChange={ this.onEditSender }
balances={ sendersBalances }
/> />
</div> </div>
); );

View File

@ -54,6 +54,7 @@ export default class TransferStore {
@observable sender = ''; @observable sender = '';
@observable senderError = null; @observable senderError = null;
@observable sendersBalances = {};
@observable total = '0.0'; @observable total = '0.0';
@observable totalError = null; @observable totalError = null;
@ -66,8 +67,6 @@ export default class TransferStore {
onClose = null; onClose = null;
senders = null; senders = null;
sendersBalances = null;
isWallet = false; isWallet = false;
wallet = null; wallet = null;

View File

@ -155,8 +155,8 @@ class Transfer extends Component {
renderDetailsPage () { renderDetailsPage () {
const { account, balance, images, senders } = this.props; const { account, balance, images, senders } = this.props;
const { valueAll, extras, recipient, recipientError, sender, senderError } = this.store; const { recipient, recipientError, sender, senderError, sendersBalances } = this.store;
const { tag, total, totalError, value, valueError } = this.store; const { valueAll, extras, tag, total, totalError, value, valueError } = this.store;
return ( return (
<Details <Details
@ -170,6 +170,7 @@ class Transfer extends Component {
recipientError={ recipientError } recipientError={ recipientError }
sender={ sender } sender={ sender }
senderError={ senderError } senderError={ senderError }
sendersBalances={ sendersBalances }
tag={ tag } tag={ tag }
total={ total } total={ total }
totalError={ totalError } totalError={ totalError }

View File

@ -15,7 +15,9 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.account { .account {
padding: 4px 0 0 0; padding: 0.25em 0;
display: flex;
align-items: center;
} }
.name { .name {
@ -27,6 +29,11 @@
padding: 0 0 0 1em; padding: 0 0 0 1em;
} }
.balance {
color: #aaa;
padding-left: 1em;
}
.image { .image {
display: inline-block; display: inline-block;
height: 32px; height: 32px;

View File

@ -21,6 +21,8 @@ import AutoComplete from '../AutoComplete';
import IdentityIcon from '../../IdentityIcon'; import IdentityIcon from '../../IdentityIcon';
import IdentityName from '../../IdentityName'; import IdentityName from '../../IdentityName';
import { fromWei } from '~/api/util/wei';
import styles from './addressSelect.css'; import styles from './addressSelect.css';
export default class AddressSelect extends Component { export default class AddressSelect extends Component {
@ -40,27 +42,46 @@ export default class AddressSelect extends Component {
value: PropTypes.string, value: PropTypes.string,
tokens: PropTypes.object, tokens: PropTypes.object,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
allowInput: PropTypes.bool allowInput: PropTypes.bool,
balances: PropTypes.object
} }
state = { state = {
autocompleteEntries: [],
entries: {}, entries: {},
addresses: [], addresses: [],
value: '' value: ''
} }
entriesFromProps (props = this.props) { entriesFromProps (props = this.props) {
const { accounts, contacts, contracts, wallets } = props; const { accounts = {}, contacts = {}, contracts = {}, wallets = {} } = props;
const entries = Object.assign({}, accounts || {}, wallets || {}, contacts || {}, contracts || {});
return entries; 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 () { componentWillMount () {
const { value } = this.props; const { value } = this.props;
const entries = this.entriesFromProps(); const { entries, autocompleteEntries } = this.entriesFromProps();
const addresses = Object.keys(entries).sort(); const addresses = Object.keys(entries).sort();
this.setState({ entries, addresses, value }); this.setState({ autocompleteEntries, entries, addresses, value });
} }
componentWillReceiveProps (newProps) { componentWillReceiveProps (newProps) {
@ -71,7 +92,7 @@ export default class AddressSelect extends Component {
render () { render () {
const { allowInput, disabled, error, hint, label } = this.props; const { allowInput, disabled, error, hint, label } = this.props;
const { entries, value } = this.state; const { autocompleteEntries, value } = this.state;
const searchText = this.getSearchText(); const searchText = this.getSearchText();
const icon = this.renderIdentityIcon(value); const icon = this.renderIdentityIcon(value);
@ -89,7 +110,7 @@ export default class AddressSelect extends Component {
onUpdateInput={ allowInput && this.onUpdateInput } onUpdateInput={ allowInput && this.onUpdateInput }
value={ searchText } value={ searchText }
filter={ this.handleFilter } filter={ this.handleFilter }
entries={ entries } entries={ autocompleteEntries }
entry={ this.getEntry() || {} } entry={ this.getEntry() || {} }
renderItem={ this.renderItem } 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 (
<div className={ styles.balance }>
{ value.toFormat(3) }<small> { 'ETH' }</small>
</div>
);
}
renderMenuItem (address) { renderMenuItem (address) {
const balance = this.props.balances
? this.renderBalance(address)
: null;
const item = ( const item = (
<div className={ styles.account }> <div className={ styles.account }>
<IdentityIcon <IdentityIcon
@ -139,6 +187,7 @@ export default class AddressSelect extends Component {
<IdentityName <IdentityName
className={ styles.name } className={ styles.name }
address={ address } /> address={ address } />
{ balance }
</div> </div>
); );
@ -155,11 +204,10 @@ export default class AddressSelect extends Component {
getSearchText () { getSearchText () {
const entry = this.getEntry(); const entry = this.getEntry();
const { value } = this.state;
return entry && entry.name return entry && entry.name
? entry.name.toUpperCase() ? entry.name.toUpperCase()
: value; : this.state.value;
} }
getEntry () { getEntry () {

View File

@ -16,11 +16,24 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import keycode from 'keycode'; 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 { PopoverAnimationVertical } from 'material-ui/Popover';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
// Hack to prevent "Unknown prop `disableFocusRipple` on <hr> tag" error
class Divider extends Component {
static muiName = MUIDivider.muiName;
render () {
return (
<div style={ { margin: '0.25em 0' } }>
<MUIDivider style={ { height: 2 } } />
</div>
);
}
}
export default class AutoComplete extends Component { export default class AutoComplete extends Component {
static propTypes = { static propTypes = {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
@ -38,15 +51,17 @@ export default class AutoComplete extends Component {
PropTypes.array, PropTypes.array,
PropTypes.object PropTypes.object
]) ])
} };
state = { state = {
lastChangedValue: undefined, lastChangedValue: undefined,
entry: null, entry: null,
open: false, open: false,
fakeBlur: false, dataSource: [],
dataSource: [] dividerBreaks: []
} };
dividersVisibility = {};
componentWillMount () { componentWillMount () {
const dataSource = this.getDataSource(); const dataSource = this.getDataSource();
@ -64,7 +79,7 @@ export default class AutoComplete extends Component {
} }
render () { 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; const { open, dataSource } = this.state;
return ( return (
@ -78,9 +93,9 @@ export default class AutoComplete extends Component {
onUpdateInput={ onUpdateInput } onUpdateInput={ onUpdateInput }
searchText={ value } searchText={ value }
onFocus={ this.onFocus } onFocus={ this.onFocus }
onBlur={ this.onBlur } onClose={ this.onClose }
animation={ PopoverAnimationVertical } animation={ PopoverAnimationVertical }
filter={ filter } filter={ this.handleFilter }
popoverProps={ { open } } popoverProps={ { open } }
openOnFocus openOnFocus
menuCloseDelay={ 0 } menuCloseDelay={ 0 }
@ -100,18 +115,76 @@ export default class AutoComplete extends Component {
? entries ? entries
: Object.values(entries); : Object.values(entries);
if (renderItem && typeof renderItem === 'function') { let currentDivider = 0;
return entriesArray.map(entry => renderItem(entry)); 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: (
<Divider />
)
};
currentDivider++;
return item;
}
let item;
if (renderItem && typeof renderItem === 'function') {
item = renderItem(entry);
} else {
item = {
text: entry,
value: (
<MenuItem
primaryText={ entry }
/>
)
};
}
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 => ({ if (item.first) {
text: entry, this.dividersVisibility = {};
value: ( }
<MenuItem
primaryText={ entry } 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) => { onKeyDown = (event) => {
@ -121,7 +194,6 @@ export default class AutoComplete extends Component {
case 'down': case 'down':
const { menu } = muiAutocomplete.refs; const { menu } = muiAutocomplete.refs;
menu && menu.handleKeyDown(event); menu && menu.handleKeyDown(event);
this.setState({ fakeBlur: true });
break; break;
case 'enter': case 'enter':
@ -155,22 +227,12 @@ export default class AutoComplete extends Component {
this.setState({ entry, open: false }); this.setState({ entry, open: false });
} }
onBlur = (event) => { onClose = (event) => {
const { onUpdateInput } = this.props; 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) { if (!onUpdateInput) {
window.setTimeout(() => { const { entry } = this.state;
const { entry, fakeBlur } = this.state; this.handleOnChange(entry);
if (fakeBlur) {
this.setState({ fakeBlur: false });
return;
}
this.handleOnChange(entry);
}, 200);
} }
} }

View File

@ -16,7 +16,7 @@
*/ */
.layout { .layout {
padding: 0.25em 0.25em 1em 0.25em; padding: 0.25em;
&>div { &>div {
margin-bottom: 0.75em; margin-bottom: 0.75em;

View File

@ -31,6 +31,10 @@
.infoline, .infoline,
.uuidline { .uuidline {
line-height: 1.618em; line-height: 1.618em;
&.bigaddress {
font-size: 1.25em;
}
} }
.infoline, .infoline,

View File

@ -32,18 +32,20 @@ export default class Header extends Component {
balance: PropTypes.object, balance: PropTypes.object,
className: PropTypes.string, className: PropTypes.string,
children: PropTypes.node, children: PropTypes.node,
isContract: PropTypes.bool isContract: PropTypes.bool,
hideName: PropTypes.bool
}; };
static defaultProps = { static defaultProps = {
className: '', className: '',
children: null, children: null,
isContract: false isContract: false,
hideName: false
}; };
render () { render () {
const { api } = this.context; const { api } = this.context;
const { account, balance, className, children } = this.props; const { account, balance, className, children, hideName } = this.props;
const { address, meta, uuid } = account; const { address, meta, uuid } = account;
if (!account) { if (!account) {
@ -60,17 +62,20 @@ export default class Header extends Component {
<IdentityIcon <IdentityIcon
address={ address } /> address={ address } />
<div className={ styles.floatleft }> <div className={ styles.floatleft }>
<ContainerTitle title={ <IdentityName address={ address } unknown /> } /> { this.renderName(address) }
<div className={ styles.addressline }>
<div className={ [ hideName ? styles.bigaddress : '', styles.addressline ].join(' ') }>
<CopyToClipboard data={ address } /> <CopyToClipboard data={ address } />
<div className={ styles.address }>{ address }</div> <div className={ styles.address }>{ address }</div>
</div> </div>
{ uuidText } { uuidText }
<div className={ styles.infoline }> <div className={ styles.infoline }>
{ meta.description } { meta.description }
</div> </div>
{ this.renderTxCount() } { this.renderTxCount() }
</div> </div>
<div className={ styles.tags }> <div className={ styles.tags }>
<Tags tags={ meta.tags } /> <Tags tags={ meta.tags } />
</div> </div>
@ -89,6 +94,18 @@ export default class Header extends Component {
); );
} }
renderName (address) {
const { hideName } = this.props;
if (hideName) {
return null;
}
return (
<ContainerTitle title={ <IdentityName address={ address } unknown /> } />
);
}
renderTxCount () { renderTxCount () {
const { balance, isContract } = this.props; const { balance, isContract } = this.props;

View File

@ -26,7 +26,7 @@ import VerifyIcon from 'material-ui/svg-icons/action/verified-user';
import { EditMeta, DeleteAccount, Shapeshift, SMSVerification, Transfer, PasswordManager } from '~/modals'; import { EditMeta, DeleteAccount, Shapeshift, SMSVerification, Transfer, PasswordManager } from '~/modals';
import { Actionbar, Button, Page } from '~/ui'; 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 Header from './Header';
import Transactions from './Transactions'; import Transactions from './Transactions';

View File

@ -153,7 +153,7 @@ export default class Summary extends Component {
const { link, noLink, account, name } = this.props; const { link, noLink, account, name } = this.props;
const { address } = account; const { address } = account;
const viewLink = `/${link || 'account'}/${address}`; const viewLink = `/${link || 'accounts'}/${address}`;
const content = ( const content = (
<IdentityName address={ address } name={ name } unknown /> <IdentityName address={ address } name={ name } unknown />

View File

@ -19,8 +19,9 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import ActionDelete from 'material-ui/svg-icons/action/delete'; import ActionDelete from 'material-ui/svg-icons/action/delete';
import ContentCreate from 'material-ui/svg-icons/content/create'; 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 { Actionbar, Button, Page } from '~/ui';
import Header from '../Account/Header'; import Header from '../Account/Header';
@ -32,7 +33,7 @@ class Address extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired, api: PropTypes.object.isRequired,
router: PropTypes.object.isRequired router: PropTypes.object.isRequired
} };
static propTypes = { static propTypes = {
setVisibleAccounts: PropTypes.func.isRequired, setVisibleAccounts: PropTypes.func.isRequired,
@ -40,12 +41,13 @@ class Address extends Component {
contacts: PropTypes.object, contacts: PropTypes.object,
balances: PropTypes.object, balances: PropTypes.object,
params: PropTypes.object params: PropTypes.object
} };
state = { state = {
showDeleteDialog: false, showDeleteDialog: false,
showEditDialog: false showEditDialog: false,
} showAdd: false
};
componentDidMount () { componentDidMount () {
this.setVisibleAccounts(); this.setVisibleAccounts();
@ -73,32 +75,69 @@ class Address extends Component {
render () { render () {
const { contacts, balances } = this.props; const { contacts, balances } = this.props;
const { address } = this.props.params; const { address } = this.props.params;
const { showDeleteDialog } = this.state;
if (Object.keys(contacts).length === 0) {
return null;
}
const contact = (contacts || {})[address]; const contact = (contacts || {})[address];
const balance = (balances || {})[address]; const balance = (balances || {})[address];
if (!contact) { return (
<div>
{ this.renderAddAddress(contact, address) }
{ this.renderEditDialog(contact) }
{ this.renderActionbar(contact) }
{ this.renderDelete(contact) }
<Page>
<Header
account={ contact || { address, meta: {} } }
balance={ balance }
hideName={ !contact }
/>
<Transactions
address={ address }
/>
</Page>
</div>
);
}
renderAddAddress (contact, address) {
if (contact) {
return null;
}
const { contacts } = this.props;
const { showAdd } = this.state;
if (!showAdd) {
return null; return null;
} }
return ( return (
<div> <AddAddress
{ this.renderEditDialog(contact) } contacts={ contacts }
{ this.renderActionbar(contact) } onClose={ this.onCloseAdd }
<Delete address={ address }
account={ contact } />
visible={ showDeleteDialog } );
route='/addresses' }
onClose={ this.closeDeleteDialog } />
<Page> renderDelete (contact) {
<Header if (!contact) {
account={ contact } return null;
balance={ balance } /> }
<Transactions
address={ address } /> const { showDeleteDialog } = this.state;
</Page>
</div> return (
<Delete
account={ contact }
visible={ showDeleteDialog }
route='/addresses'
onClose={ this.closeDeleteDialog }
/>
); );
} }
@ -116,17 +155,27 @@ class Address extends Component {
onClick={ this.showDeleteDialog } /> onClick={ this.showDeleteDialog } />
]; ];
const addToBook = (
<Button
key='newAddress'
icon={ <ContentAdd /> }
label='save address'
onClick={ this.onOpenAdd }
/>
);
return ( return (
<Actionbar <Actionbar
title='Address Information' title='Address Information'
buttons={ !contact ? [] : buttons } /> buttons={ !contact ? [ addToBook ] : buttons }
/>
); );
} }
renderEditDialog (contact) { renderEditDialog (contact) {
const { showEditDialog } = this.state; const { showEditDialog } = this.state;
if (!showEditDialog) { if (!contact || !showEditDialog) {
return null; return null;
} }
@ -151,6 +200,16 @@ class Address extends Component {
showDeleteDialog = () => { showDeleteDialog = () => {
this.setState({ showDeleteDialog: true }); this.setState({ showDeleteDialog: true });
} }
onOpenAdd = () => {
this.setState({
showAdd: true
});
}
onCloseAdd = () => {
this.setState({ showAdd: false });
}
} }
function mapStateToProps (state) { function mapStateToProps (state) {

View File

@ -23,7 +23,7 @@ import { uniq, isEqual } from 'lodash';
import List from '../Accounts/List'; import List from '../Accounts/List';
import Summary from '../Accounts/Summary'; import Summary from '../Accounts/Summary';
import { AddAddress } from '~/modals'; import { AddAddress } from '~/modals';
import { Actionbar, ActionbarExport, ActionbarImport, ActionbarSearch, ActionbarSort, Button, Page } from '~/ui'; import { Actionbar, ActionbarExport, ActionbarImport, ActionbarSearch, ActionbarSort, Button, Page, Loading } from '~/ui';
import { setVisibleAccounts } from '~/redux/providers/personalActions'; import { setVisibleAccounts } from '~/redux/providers/personalActions';
import styles from './addresses.css'; import styles from './addresses.css';
@ -72,27 +72,40 @@ class Addresses extends Component {
} }
render () { render () {
const { balances, contacts, hasContacts } = this.props;
const { searchValues, sortOrder } = this.state;
return ( return (
<div> <div>
{ this.renderActionbar() } { this.renderActionbar() }
{ this.renderAddAddress() } { this.renderAddAddress() }
<Page> <Page>
<List { this.renderAccountsList() }
link='address'
search={ searchValues }
accounts={ contacts }
balances={ balances }
empty={ !hasContacts }
order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken } />
</Page> </Page>
</div> </div>
); );
} }
renderAccountsList () {
const { balances, contacts, hasContacts } = this.props;
const { searchValues, sortOrder } = this.state;
if (hasContacts && Object.keys(balances).length === 0) {
return (
<Loading />
);
}
return (
<List
link='addresses'
search={ searchValues }
accounts={ contacts }
balances={ balances }
empty={ !hasContacts }
order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken }
/>
);
}
renderSortButton () { renderSortButton () {
const onChange = (sortOrder) => { const onChange = (sortOrder) => {
this.setState({ sortOrder }); this.setState({ sortOrder });

View File

@ -30,24 +30,34 @@
} }
} }
.tabs button, .tabLink {
display: flex;
> * {
flex: 1;
}
&:hover {
background: rgba(0, 0, 0, 0.4) !important;
}
&.tabactive, &.tabactive:hover {
background: rgba(0, 0, 0, 0.25) !important;
border-radius: 4px 4px 0 0;
* {
color: white !important;
}
}
}
.tabLink,
.settings, .settings,
.logo, .logo,
.last { .last {
background: rgba(0, 0, 0, 0.5) !important; /* rgba(0, 0, 0, 0.25) !important; */ background: rgba(0, 0, 0, 0.5) !important; /* rgba(0, 0, 0, 0.25) !important; */
} }
.tabs button:hover {
background: rgba(0, 0, 0, 0.4) !important;
}
button.tabactive,
button.tabactive:hover {
color: white !important;
background: rgba(0, 0, 0, 0.25) !important;
border-radius: 4px 4px 0 0;
}
.tabbarTooltip { .tabbarTooltip {
left: 3.3em; left: 3.3em;
top: 0.5em; top: 0.5em;

View File

@ -16,7 +16,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { Link } from 'react-router';
import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar'; import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar';
import { Tab as MUITab } from 'material-ui/Tabs'; import { Tab as MUITab } from 'material-ui/Tabs';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
@ -26,41 +26,26 @@ import { Badge, Tooltip } from '~/ui';
import styles from './tabBar.css'; import styles from './tabBar.css';
import imagesEthcoreBlock from '../../../../assets/images/parity-logo-white-no-text.svg'; import imagesEthcoreBlock from '../../../../assets/images/parity-logo-white-no-text.svg';
const TABMAP = {
accounts: 'account',
wallet: 'account',
addresses: 'address',
apps: 'app',
contracts: 'contract',
deploy: 'contract'
};
class Tab extends Component { class Tab extends Component {
static propTypes = { static propTypes = {
active: PropTypes.bool,
view: PropTypes.object, view: PropTypes.object,
children: PropTypes.node, children: PropTypes.node,
pendings: PropTypes.number, pendings: PropTypes.number
onChange: PropTypes.func
}; };
shouldComponentUpdate (nextProps) { shouldComponentUpdate (nextProps) {
return nextProps.active !== this.props.active || return (nextProps.view.id === 'signer' && nextProps.pendings !== this.props.pendings);
(nextProps.view.id === 'signer' && nextProps.pendings !== this.props.pendings);
} }
render () { render () {
const { active, view, children } = this.props; const { view, children } = this.props;
const label = this.getLabel(view); const label = this.getLabel(view);
return ( return (
<MUITab <MUITab
className={ active ? styles.tabactive : '' }
selected={ active }
icon={ view.icon } icon={ view.icon }
label={ label } label={ label }
onTouchTap={ this.handleClick }
> >
{ children } { children }
</MUITab> </MUITab>
@ -118,11 +103,6 @@ class Tab extends Component {
return this.renderLabel(label, null); return this.renderLabel(label, null);
} }
handleClick = () => {
const { onChange, view } = this.props;
onChange(view);
}
} }
class TabBar extends Component { class TabBar extends Component {
@ -132,7 +112,6 @@ class TabBar extends Component {
static propTypes = { static propTypes = {
views: PropTypes.array.isRequired, views: PropTypes.array.isRequired,
hash: PropTypes.string.isRequired,
pending: PropTypes.array, pending: PropTypes.array,
isTest: PropTypes.bool, isTest: PropTypes.bool,
netChain: PropTypes.string netChain: PropTypes.string
@ -142,34 +121,11 @@ class TabBar extends Component {
pending: [] pending: []
}; };
state = {
activeViewId: ''
};
setActiveView (props = this.props) {
const { hash, views } = props;
const view = views.find((view) => view.value === hash);
this.setState({ activeViewId: view.id });
}
componentWillMount () {
this.setActiveView();
}
componentWillReceiveProps (nextProps) {
if (nextProps.hash !== this.props.hash) {
this.setActiveView(nextProps);
}
}
shouldComponentUpdate (nextProps, nextState) { shouldComponentUpdate (nextProps, nextState) {
const prevViews = this.props.views.map((v) => v.id).sort(); const prevViews = this.props.views.map((v) => v.id).sort();
const nextViews = nextProps.views.map((v) => v.id).sort(); const nextViews = nextProps.views.map((v) => v.id).sort();
return (nextProps.hash !== this.props.hash) || return (nextProps.pending.length !== this.props.pending.length) ||
(nextProps.pending.length !== this.props.pending.length) ||
(nextState.activeViewId !== this.state.activeViewId) ||
(!isEqual(prevViews, nextViews)); (!isEqual(prevViews, nextViews));
} }
@ -206,7 +162,6 @@ class TabBar extends Component {
renderTabs () { renderTabs () {
const { views, pending } = this.props; const { views, pending } = this.props;
const { activeViewId } = this.state;
const items = views const items = views
.map((view, index) => { .map((view, index) => {
@ -216,60 +171,66 @@ class TabBar extends Component {
) )
: null; : null;
const active = activeViewId === view.id;
return ( return (
<Tab <Link
active={ active }
view={ view }
onChange={ this.onChange }
key={ view.id } key={ view.id }
pendings={ pending.length } to={ view.route }
activeClassName={ styles.tabactive }
className={ styles.tabLink }
> >
{ body } <Tab
</Tab> view={ view }
pendings={ pending.length }
>
{ body }
</Tab>
</Link>
); );
}); });
return ( return (
<div <div className={ styles.tabs }>
className={ styles.tabs }
onChange={ this.onChange }>
{ items } { items }
</div> </div>
); );
} }
onChange = (view) => {
const { router } = this.context;
router.push(view.route);
this.setState({ activeViewId: view.id });
}
} }
function mapStateToProps (state) { function mapStateToProps (initState) {
const { views } = state.settings; const { views } = initState.settings;
const filteredViews = Object let filteredViewIds = Object
.keys(views) .keys(views)
.filter((id) => views[id].fixed || views[id].active) .filter((id) => views[id].fixed || views[id].active);
let filteredViews = filteredViewIds
.map((id) => ({ .map((id) => ({
...views[id], ...views[id],
id id
})); }));
const windowHash = (window.location.hash || '').split('?')[0].split('/')[1]; return (state) => {
const hash = TABMAP[windowHash] || windowHash; const { views } = state.settings;
return { views: filteredViews, hash }; const viewIds = Object
} .keys(views)
.filter((id) => views[id].fixed || views[id].active);
function mapDispatchToProps (dispatch) { if (isEqual(viewIds, filteredViewIds)) {
return bindActionCreators({}, dispatch); return { views: filteredViews };
}
filteredViewIds = viewIds;
filteredViews = viewIds
.map((id) => ({
...views[id],
id
}));
return { views: filteredViews };
};
} }
export default connect( export default connect(
mapStateToProps, mapStateToProps
mapDispatchToProps
)(TabBar); )(TabBar);

View File

@ -19,4 +19,5 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; min-height: 100vh;
padding-bottom: 1em;
} }

View File

@ -85,7 +85,7 @@ class Contracts extends Component {
{ this.renderDeployContract() } { this.renderDeployContract() }
<Page> <Page>
<List <List
link='contract' link='contracts'
search={ searchValues } search={ searchValues }
accounts={ contracts } accounts={ contracts }
balances={ balances } balances={ balances }
@ -142,12 +142,12 @@ class Contracts extends Component {
label='deploy contract' label='deploy contract'
onClick={ this.onDeployContract } />, onClick={ this.onDeployContract } />,
<Link <Link
to='/contracts/write' to='/contracts/develop'
key='writeContract' key='writeContract'
> >
<Button <Button
icon={ <FileIcon /> } icon={ <FileIcon /> }
label='write contract' label='develop contract'
/> />
</Link>, </Link>,

View File

@ -72,9 +72,17 @@ export default class Dapps extends Component {
] } ] }
/> />
<Page> <Page>
{ this.renderList(this.store.visibleLocal) } <div>
{ this.renderList(this.store.visibleBuiltin) } { this.renderList(this.store.visibleLocal) }
{ this.renderList(this.store.visibleNetwork, externalOverlay) } </div>
<div>
{ this.renderList(this.store.visibleBuiltin) }
</div>
<div>
{ this.renderList(this.store.visibleNetwork, externalOverlay) }
</div>
</Page> </Page>
</div> </div>
); );

View File

@ -15,8 +15,8 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
try { try {
var Api = require('../.npmjs/library.js').Api; var Api = require('../.npmjs/parity/library.js').Api;
var Abi = require('../.npmjs/library.js').Abi; var Abi = require('../.npmjs/parity/library.js').Abi;
if (typeof Api !== 'function') { if (typeof Api !== 'function') {
throw new Error('No Api'); throw new Error('No Api');

View File

@ -23,14 +23,27 @@ const Shared = require('./shared');
const ENV = process.env.NODE_ENV || 'development'; const ENV = process.env.NODE_ENV || 'development';
const isProd = ENV === 'production'; 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 = { module.exports = {
context: path.join(__dirname, '../src'), context: path.join(__dirname, '../src'),
target: 'node', target: 'node',
entry: 'library.js', entry: `library.${SRC}.js`,
output: { output: {
path: path.join(__dirname, '../.npmjs'), path: OUTPUT_PATH,
filename: 'library.js', filename: 'library.js',
library: 'Parity', library: LIBRARY,
libraryTarget: 'umd', libraryTarget: 'umd',
umdNamedDefine: true umdNamedDefine: true
}, },
@ -66,19 +79,52 @@ module.exports = {
plugins: Shared.getPlugins().concat([ plugins: Shared.getPlugins().concat([
new CopyWebpackPlugin([ new CopyWebpackPlugin([
{ {
from: '../parity.package.json', from: `../npm/${SRC}/package.json`,
to: 'package.json', to: 'package.json',
transform: function (content, path) { transform: function (content, path) {
const json = JSON.parse(content.toString()); const json = JSON.parse(content.toString());
json.version = packageJson.version; 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'); return new Buffer(JSON.stringify(json, null, ' '), 'utf-8');
} }
}, },
{ {
from: '../LICENSE' 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' to: 'README.md'
} }
], { copyUnmodified: true }) ], { copyUnmodified: true })

View File

@ -36,6 +36,29 @@ function getBabelrc () {
// [ "es2015", { "modules": false } ] // [ "es2015", { "modules": false } ]
babelrc.presets[es2015Index] = [ 'es2015', { modules: false } ]; babelrc.presets[es2015Index] = [ 'es2015', { modules: false } ];
babelrc['babelrc'] = 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; return babelrc;
} }

View File

@ -128,7 +128,13 @@ impl Default for UserDefaults {
impl UserDefaults { impl UserDefaults {
pub fn load<P>(path: P) -> Result<Self, String> where P: AsRef<Path> { pub fn load<P>(path: P) -> Result<Self, String> where P: AsRef<Path> {
match File::open(path) { 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()), _ => Ok(UserDefaults::default()),
} }
} }