Merge branch 'master' into check-updates

This commit is contained in:
Gav Wood 2016-12-07 19:19:44 +01:00
commit 8903384840
No known key found for this signature in database
GPG Key ID: C49C1ACA1CC9B252
139 changed files with 5289 additions and 761 deletions

16
Cargo.lock generated
View File

@ -24,7 +24,7 @@ dependencies = [
"ethcore-stratum 1.4.0",
"ethcore-util 1.5.0",
"ethsync 1.5.0",
"fdlimit 0.1.0",
"fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)",
"isatty 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -687,7 +687,8 @@ dependencies = [
[[package]]
name = "fdlimit"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -843,7 +844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "jsonrpc-core"
version = "4.0.0"
source = "git+https://github.com/ethcore/jsonrpc.git#20c7e55b84d7fd62732f062dc3058e1b71133e4a"
source = "git+https://github.com/ethcore/jsonrpc.git#1500da1b9613a0a17fc0109d825f3ccc60199a53"
dependencies = [
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
@ -855,7 +856,7 @@ dependencies = [
[[package]]
name = "jsonrpc-http-server"
version = "6.1.1"
source = "git+https://github.com/ethcore/jsonrpc.git#20c7e55b84d7fd62732f062dc3058e1b71133e4a"
source = "git+https://github.com/ethcore/jsonrpc.git#1500da1b9613a0a17fc0109d825f3ccc60199a53"
dependencies = [
"hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)",
"jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
@ -866,7 +867,7 @@ dependencies = [
[[package]]
name = "jsonrpc-ipc-server"
version = "0.2.4"
source = "git+https://github.com/ethcore/jsonrpc.git#20c7e55b84d7fd62732f062dc3058e1b71133e4a"
source = "git+https://github.com/ethcore/jsonrpc.git#1500da1b9613a0a17fc0109d825f3ccc60199a53"
dependencies = [
"bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
@ -881,7 +882,7 @@ dependencies = [
[[package]]
name = "jsonrpc-tcp-server"
version = "0.1.0"
source = "git+https://github.com/ethcore/jsonrpc.git#20c7e55b84d7fd62732f062dc3058e1b71133e4a"
source = "git+https://github.com/ethcore/jsonrpc.git#1500da1b9613a0a17fc0109d825f3ccc60199a53"
dependencies = [
"bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1273,7 +1274,7 @@ dependencies = [
[[package]]
name = "parity-ui-precompiled"
version = "1.4.0"
source = "git+https://github.com/ethcore/js-precompiled.git#7700411d2b0ba1372e0d6cf72a84ecf873a181f3"
source = "git+https://github.com/ethcore/js-precompiled.git#1690a80b9f56e33c8344f28eb637bacd97c9da14"
dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -2047,6 +2048,7 @@ dependencies = [
"checksum env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "aba65b63ffcc17ffacd6cf5aa843da7c5a25e3bd4bbe0b7def8b214e411250e5"
"checksum eth-secp256k1 0.5.4 (git+https://github.com/ethcore/rust-secp256k1)" = "<none>"
"checksum ethabi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f7b0c53453517f620847be51943db329276ae52f2e210cfc659e81182864be2f"
"checksum fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b1ee15a7050e5580b3712877157068ea713b245b080ff302ae2ca973cfcd9baa"
"checksum flate2 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "3eeb481e957304178d2e782f2da1257f1434dfecbae883bafb61ada2a9fea3bb"
"checksum gcc 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)" = "91ecd03771effb0c968fd6950b37e89476a578aaf1c70297d8e92b6516ec3312"
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"

View File

@ -30,7 +30,7 @@ serde = "0.8.0"
serde_json = "0.8.0"
hyper = { version = "0.9", default-features = false }
ctrlc = { git = "https://github.com/ethcore/rust-ctrlc.git" }
fdlimit = { path = "util/fdlimit" }
fdlimit = "0.1"
ethcore = { path = "ethcore" }
ethcore-util = { path = "util" }
ethsync = { path = "sync" }

View File

@ -5,6 +5,10 @@ license = "GPL-3.0"
name = "ethcore-light"
version = "1.5.0"
authors = ["Ethcore <admin@ethcore.io>"]
build = "build.rs"
[build-dependencies]
"ethcore-ipc-codegen" = { path = "../../ipc/codegen" }
[dependencies]
log = "0.3"
@ -12,5 +16,6 @@ ethcore = { path = ".." }
ethcore-util = { path = "../../util" }
ethcore-network = { path = "../../util/network" }
ethcore-io = { path = "../../util/io" }
ethcore-ipc = { path = "../../ipc/rpc" }
rlp = { path = "../../util/rlp" }
time = "0.1"

View File

@ -14,8 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { PropTypes } from 'react';
extern crate ethcore_ipc_codegen;
export default function nullableProptype (type) {
return PropTypes.oneOfType([ PropTypes.oneOf([ null ]), type ]);
fn main() {
ethcore_ipc_codegen::derive_binary("src/types/mod.rs.in").unwrap();
}

View File

@ -101,7 +101,7 @@ impl Provider for Client {
Vec::new()
}
fn code(&self, _req: request::ContractCodes) -> Vec<Bytes> {
fn contract_code(&self, _req: request::ContractCodes) -> Vec<Bytes> {
Vec::new()
}

View File

@ -28,20 +28,25 @@
//! It starts by performing a header-only sync, verifying random samples
//! of members of the chain to varying degrees.
// TODO: remove when integrating with parity.
// TODO: remove when integrating with the rest of parity.
#![allow(dead_code)]
pub mod client;
pub mod net;
pub mod provider;
pub mod request;
extern crate ethcore_util as util;
extern crate ethcore_network as network;
extern crate ethcore_io as io;
extern crate ethcore;
extern crate rlp;
extern crate time;
mod types;
pub use self::provider::Provider;
pub use types::les_request as request;
#[macro_use]
extern crate log;
extern crate ethcore;
extern crate ethcore_util as util;
extern crate ethcore_network as network;
extern crate ethcore_io as io;
extern crate ethcore_ipc as ipc;
extern crate rlp;
extern crate time;

View File

@ -206,6 +206,39 @@ impl FlowParams {
cost.0 + (amount * cost.1)
}
/// Compute the maximum number of costs of a specific kind which can be made
/// with the given buffer.
/// Saturates at `usize::max()`. This is not a problem in practice because
/// this amount of requests is already prohibitively large.
pub fn max_amount(&self, buffer: &Buffer, kind: request::Kind) -> usize {
use util::Uint;
use std::usize;
let cost = match kind {
request::Kind::Headers => &self.costs.headers,
request::Kind::Bodies => &self.costs.bodies,
request::Kind::Receipts => &self.costs.receipts,
request::Kind::StateProofs => &self.costs.state_proofs,
request::Kind::Codes => &self.costs.contract_codes,
request::Kind::HeaderProofs => &self.costs.header_proofs,
};
let start = buffer.current();
if start <= cost.0 {
return 0;
} else if cost.1 == U256::zero() {
return usize::MAX;
}
let max = (start - cost.0) / cost.1;
if max >= usize::MAX.into() {
usize::MAX
} else {
max.as_u64() as usize
}
}
/// Create initial buffer parameter.
pub fn create_buffer(&self) -> Buffer {
Buffer {
@ -228,6 +261,16 @@ impl FlowParams {
buf.estimate = ::std::cmp::min(self.limit, buf.estimate + (elapsed * self.recharge));
}
/// Refund some buffer which was previously deducted.
/// Does not update the recharge timestamp.
pub fn refund(&self, buf: &mut Buffer, refund_amount: U256) {
buf.estimate = buf.estimate + refund_amount;
if buf.estimate > self.limit {
buf.estimate = self.limit
}
}
}
#[cfg(test)]

View File

@ -52,6 +52,8 @@ pub enum Error {
UnexpectedHandshake,
/// Peer on wrong network (wrong NetworkId or genesis hash)
WrongNetwork,
/// Unknown peer.
UnknownPeer,
}
impl Error {
@ -64,6 +66,7 @@ impl Error {
Error::UnrecognizedPacket(_) => Punishment::Disconnect,
Error::UnexpectedHandshake => Punishment::Disconnect,
Error::WrongNetwork => Punishment::Disable,
Error::UnknownPeer => Punishment::Disconnect,
}
}
}
@ -89,6 +92,7 @@ impl fmt::Display for Error {
Error::UnrecognizedPacket(code) => write!(f, "Unrecognized packet: 0x{:x}", code),
Error::UnexpectedHandshake => write!(f, "Unexpected handshake"),
Error::WrongNetwork => write!(f, "Wrong network"),
Error::UnknownPeer => write!(f, "unknown peer"),
}
}
}

View File

@ -19,27 +19,28 @@
//! This uses a "Provider" to answer requests.
//! See https://github.com/ethcore/parity/wiki/Light-Ethereum-Subprotocol-(LES)
use ethcore::transaction::SignedTransaction;
use io::TimerToken;
use network::{NetworkProtocolHandler, NetworkContext, NetworkError, PeerId};
use rlp::{RlpStream, Stream, UntrustedRlp, View};
use util::hash::H256;
use util::RwLock;
use util::{Mutex, RwLock, U256};
use time::SteadyTime;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::{AtomicUsize, Ordering};
use provider::Provider;
use request::{self, Request};
use self::buffer_flow::{Buffer, FlowParams};
use self::error::{Error, Punishment};
use self::status::{Status, Capabilities};
mod buffer_flow;
mod error;
mod status;
pub use self::status::Announcement;
pub use self::status::{Status, Capabilities, Announcement, NetworkId};
const TIMEOUT: TimerToken = 0;
const TIMEOUT_INTERVAL_MS: u64 = 1000;
@ -86,6 +87,10 @@ mod packet {
pub const HEADER_PROOFS: u8 = 0x0e;
}
/// A request id.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ReqId(usize);
// A pending peer: one we've sent our status to but
// may not have received one for.
struct PendingPeer {
@ -103,32 +108,162 @@ struct Peer {
sent_head: H256, // last head we've given them.
}
impl Peer {
// check the maximum cost of a request, returning an error if there's
// not enough buffer left.
// returns the calculated maximum cost.
fn deduct_max(&mut self, flow_params: &FlowParams, kind: request::Kind, max: usize) -> Result<U256, Error> {
flow_params.recharge(&mut self.local_buffer);
let max_cost = flow_params.compute_cost(kind, max);
try!(self.local_buffer.deduct_cost(max_cost));
Ok(max_cost)
}
// refund buffer for a request. returns new buffer amount.
fn refund(&mut self, flow_params: &FlowParams, amount: U256) -> U256 {
flow_params.refund(&mut self.local_buffer, amount);
self.local_buffer.current()
}
// recharge remote buffer with remote flow params.
fn recharge_remote(&mut self) {
let flow = &mut self.remote_flow;
flow.recharge(&mut self.remote_buffer);
}
}
/// An LES event handler.
pub trait Handler: Send + Sync {
/// Called when a peer connects.
fn on_connect(&self, _id: PeerId, _status: &Status, _capabilities: &Capabilities) { }
/// Called when a peer disconnects
fn on_disconnect(&self, _id: PeerId) { }
/// Called when a peer makes an announcement.
fn on_announcement(&self, _id: PeerId, _announcement: &Announcement) { }
/// Called when a peer requests relay of some transactions.
fn on_transactions(&self, _id: PeerId, _relay: &[SignedTransaction]) { }
}
// a request and the time it was made.
struct Requested {
request: Request,
timestamp: SteadyTime,
}
/// Protocol parameters.
pub struct Params {
/// Genesis hash.
pub genesis_hash: H256,
/// Network id.
pub network_id: NetworkId,
/// Buffer flow parameters.
pub flow_params: FlowParams,
/// Initial capabilities.
pub capabilities: Capabilities,
}
/// This is an implementation of the light ethereum network protocol, abstracted
/// over a `Provider` of data and a p2p network.
///
/// This is simply designed for request-response purposes. Higher level uses
/// of the protocol, such as synchronization, will function as wrappers around
/// this system.
//
// LOCK ORDER:
// Locks must be acquired in the order declared, and when holding a read lock
// on the peers, only one peer may be held at a time.
pub struct LightProtocol {
provider: Box<Provider>,
genesis_hash: H256,
network_id: status::NetworkId,
network_id: NetworkId,
pending_peers: RwLock<HashMap<PeerId, PendingPeer>>,
peers: RwLock<HashMap<PeerId, Peer>>,
pending_requests: RwLock<HashMap<usize, Request>>,
peers: RwLock<HashMap<PeerId, Mutex<Peer>>>,
pending_requests: RwLock<HashMap<usize, Requested>>,
capabilities: RwLock<Capabilities>,
flow_params: FlowParams, // assumed static and same for every peer.
handlers: Vec<Box<Handler>>,
req_id: AtomicUsize,
}
impl LightProtocol {
/// Create a new instance of the protocol manager.
pub fn new(provider: Box<Provider>, params: Params) -> Self {
LightProtocol {
provider: provider,
genesis_hash: params.genesis_hash,
network_id: params.network_id,
pending_peers: RwLock::new(HashMap::new()),
peers: RwLock::new(HashMap::new()),
pending_requests: RwLock::new(HashMap::new()),
capabilities: RwLock::new(params.capabilities),
flow_params: params.flow_params,
handlers: Vec::new(),
req_id: AtomicUsize::new(0),
}
}
/// Check the maximum amount of requests of a specific type
/// which a peer would be able to serve.
pub fn max_requests(&self, peer: PeerId, kind: request::Kind) -> Option<usize> {
self.peers.read().get(&peer).map(|peer| {
let mut peer = peer.lock();
peer.recharge_remote();
peer.remote_flow.max_amount(&peer.remote_buffer, kind)
})
}
/// Make a request to a peer.
///
/// Fails on: nonexistent peer, network error,
/// insufficient buffer. Does not check capabilities before sending.
/// On success, returns a request id which can later be coordinated
/// with an event.
pub fn request_from(&self, io: &NetworkContext, peer_id: &PeerId, request: Request) -> Result<ReqId, Error> {
let peers = self.peers.read();
let peer = try!(peers.get(peer_id).ok_or_else(|| Error::UnknownPeer));
let mut peer = peer.lock();
peer.recharge_remote();
let max = peer.remote_flow.compute_cost(request.kind(), request.amount());
try!(peer.remote_buffer.deduct_cost(max));
let req_id = self.req_id.fetch_add(1, Ordering::SeqCst);
let packet_data = encode_request(&request, req_id);
let packet_id = match request.kind() {
request::Kind::Headers => packet::GET_BLOCK_HEADERS,
request::Kind::Bodies => packet::GET_BLOCK_BODIES,
request::Kind::Receipts => packet::GET_RECEIPTS,
request::Kind::StateProofs => packet::GET_PROOFS,
request::Kind::Codes => packet::GET_CONTRACT_CODES,
request::Kind::HeaderProofs => packet::GET_HEADER_PROOFS,
};
try!(io.send(*peer_id, packet_id, packet_data));
peer.current_asking.insert(req_id);
self.pending_requests.write().insert(req_id, Requested {
request: request,
timestamp: SteadyTime::now(),
});
Ok(ReqId(req_id))
}
/// Make an announcement of new chain head and capabilities to all peers.
/// The announcement is expected to be valid.
pub fn make_announcement(&self, mut announcement: Announcement, io: &NetworkContext) {
pub fn make_announcement(&self, io: &NetworkContext, mut announcement: Announcement) {
let mut reorgs_map = HashMap::new();
// update stored capabilities
self.capabilities.write().update_from(&announcement);
// calculate reorg info and send packets
for (peer_id, peer_info) in self.peers.write().iter_mut() {
for (peer_id, peer_info) in self.peers.read().iter() {
let mut peer_info = peer_info.lock();
let reorg_depth = reorgs_map.entry(peer_info.sent_head)
.or_insert_with(|| {
match self.provider.reorg_depth(&announcement.head_hash, &peer_info.sent_head) {
@ -151,6 +286,14 @@ impl LightProtocol {
}
}
}
/// Add an event handler.
/// Ownership will be transferred to the protocol structure,
/// and the handler will be kept alive as long as it is.
/// These are intended to be added at the beginning of the
pub fn add_handler(&mut self, handler: Box<Handler>) {
self.handlers.push(handler);
}
}
impl LightProtocol {
@ -173,7 +316,11 @@ impl LightProtocol {
fn on_disconnect(&self, peer: PeerId) {
// TODO: reassign all requests assigned to this peer.
self.pending_peers.write().remove(&peer);
self.peers.write().remove(&peer);
if self.peers.write().remove(&peer).is_some() {
for handler in &self.handlers {
handler.on_disconnect(peer)
}
}
}
// send status to a peer.
@ -219,15 +366,19 @@ impl LightProtocol {
return Err(Error::WrongNetwork);
}
self.peers.write().insert(*peer, Peer {
self.peers.write().insert(*peer, Mutex::new(Peer {
local_buffer: self.flow_params.create_buffer(),
remote_buffer: flow_params.create_buffer(),
current_asking: HashSet::new(),
status: status,
capabilities: capabilities,
status: status.clone(),
capabilities: capabilities.clone(),
remote_flow: flow_params,
sent_head: pending.sent_head,
});
}));
for handler in &self.handlers {
handler.on_connect(*peer, &status, &capabilities)
}
Ok(())
}
@ -240,13 +391,15 @@ impl LightProtocol {
}
let announcement = try!(status::parse_announcement(data));
let mut peers = self.peers.write();
let peers = self.peers.read();
let peer_info = match peers.get_mut(peer) {
let peer_info = match peers.get(peer) {
Some(info) => info,
None => return Ok(()),
};
let mut peer_info = peer_info.lock();
// update status.
{
// TODO: punish peer if they've moved backwards.
@ -259,15 +412,11 @@ impl LightProtocol {
}
// update capabilities.
{
let caps = &mut peer_info.capabilities;
caps.serve_headers = caps.serve_headers || announcement.serve_headers;
caps.serve_state_since = caps.serve_state_since.or(announcement.serve_state_since);
caps.serve_chain_since = caps.serve_chain_since.or(announcement.serve_chain_since);
caps.tx_relay = caps.tx_relay || announcement.tx_relay;
}
peer_info.capabilities.update_from(&announcement);
// TODO: notify listeners if new best block.
for handler in &self.handlers {
handler.on_announcement(*peer, &announcement);
}
Ok(())
}
@ -276,45 +425,39 @@ impl LightProtocol {
fn get_block_headers(&self, peer: &PeerId, io: &NetworkContext, data: UntrustedRlp) -> Result<(), Error> {
const MAX_HEADERS: usize = 512;
let mut present_buffer = match self.peers.read().get(peer) {
Some(peer) => peer.local_buffer.clone(),
let peers = self.peers.read();
let peer = match peers.get(peer) {
Some(peer) => peer,
None => {
debug!(target: "les", "Ignoring announcement from unknown peer");
debug!(target: "les", "Ignoring request from unknown peer");
return Ok(())
}
};
self.flow_params.recharge(&mut present_buffer);
let mut peer = peer.lock();
let req_id: u64 = try!(data.val_at(0));
let req = request::Headers {
block: {
let block = {
let rlp = try!(data.at(1));
(try!(rlp.val_at(0)), try!(rlp.val_at(1)))
},
};
let req = request::Headers {
block_num: block.0,
block_hash: block.1,
max: ::std::cmp::min(MAX_HEADERS, try!(data.val_at(2))),
skip: try!(data.val_at(3)),
reverse: try!(data.val_at(4)),
};
let max_cost = self.flow_params.compute_cost(request::Kind::Headers, req.max);
try!(present_buffer.deduct_cost(max_cost));
let max_cost = try!(peer.deduct_max(&self.flow_params, request::Kind::Headers, req.max));
let response = self.provider.block_headers(req);
let actual_cost = self.flow_params.compute_cost(request::Kind::Headers, response.len());
assert!(max_cost >= actual_cost, "Actual cost exceeded maximum computed cost.");
let cur_buffer = match self.peers.write().get_mut(peer) {
Some(peer) => {
self.flow_params.recharge(&mut peer.local_buffer);
try!(peer.local_buffer.deduct_cost(actual_cost));
peer.local_buffer.current()
}
None => {
debug!(target: "les", "peer disconnected during serving of request.");
return Ok(())
}
};
let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost);
io.respond(packet::BLOCK_HEADERS, {
let mut stream = RlpStream::new_list(response.len() + 2);
stream.append(&req_id).append(&cur_buffer);
@ -336,39 +479,30 @@ impl LightProtocol {
fn get_block_bodies(&self, peer: &PeerId, io: &NetworkContext, data: UntrustedRlp) -> Result<(), Error> {
const MAX_BODIES: usize = 256;
let mut present_buffer = match self.peers.read().get(peer) {
Some(peer) => peer.local_buffer.clone(),
let peers = self.peers.read();
let peer = match peers.get(peer) {
Some(peer) => peer,
None => {
debug!(target: "les", "Ignoring announcement from unknown peer");
debug!(target: "les", "Ignoring request from unknown peer");
return Ok(())
}
};
let mut peer = peer.lock();
self.flow_params.recharge(&mut present_buffer);
let req_id: u64 = try!(data.val_at(0));
let req = request::Bodies {
block_hashes: try!(data.iter().skip(1).take(MAX_BODIES).map(|x| x.as_val()).collect())
};
let max_cost = self.flow_params.compute_cost(request::Kind::Bodies, req.block_hashes.len());
try!(present_buffer.deduct_cost(max_cost));
let max_cost = try!(peer.deduct_max(&self.flow_params, request::Kind::Bodies, req.block_hashes.len()));
let response = self.provider.block_bodies(req);
let response_len = response.iter().filter(|x| &x[..] != &::rlp::EMPTY_LIST_RLP).count();
let actual_cost = self.flow_params.compute_cost(request::Kind::Bodies, response_len);
assert!(max_cost >= actual_cost, "Actual cost exceeded maximum computed cost.");
let cur_buffer = match self.peers.write().get_mut(peer) {
Some(peer) => {
self.flow_params.recharge(&mut peer.local_buffer);
try!(peer.local_buffer.deduct_cost(actual_cost));
peer.local_buffer.current()
}
None => {
debug!(target: "les", "peer disconnected during serving of request.");
return Ok(())
}
};
let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost);
io.respond(packet::BLOCK_BODIES, {
let mut stream = RlpStream::new_list(response.len() + 2);
@ -388,8 +522,44 @@ impl LightProtocol {
}
// Handle a request for receipts.
fn get_receipts(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> {
unimplemented!()
fn get_receipts(&self, peer: &PeerId, io: &NetworkContext, data: UntrustedRlp) -> Result<(), Error> {
const MAX_RECEIPTS: usize = 256;
let peers = self.peers.read();
let peer = match peers.get(peer) {
Some(peer) => peer,
None => {
debug!(target: "les", "Ignoring request from unknown peer");
return Ok(())
}
};
let mut peer = peer.lock();
let req_id: u64 = try!(data.val_at(0));
let req = request::Receipts {
block_hashes: try!(data.iter().skip(1).take(MAX_RECEIPTS).map(|x| x.as_val()).collect())
};
let max_cost = try!(peer.deduct_max(&self.flow_params, request::Kind::Receipts, req.block_hashes.len()));
let response = self.provider.receipts(req);
let response_len = response.iter().filter(|x| &x[..] != &::rlp::EMPTY_LIST_RLP).count();
let actual_cost = self.flow_params.compute_cost(request::Kind::Receipts, response_len);
assert!(max_cost >= actual_cost, "Actual cost exceeded maximum computed cost.");
let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost);
io.respond(packet::RECEIPTS, {
let mut stream = RlpStream::new_list(response.len() + 2);
stream.append(&req_id).append(&cur_buffer);
for receipts in response {
stream.append_raw(&receipts, 1);
}
stream.out()
}).map_err(Into::into)
}
// Receive a response for receipts.
@ -398,8 +568,55 @@ impl LightProtocol {
}
// Handle a request for proofs.
fn get_proofs(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> {
unimplemented!()
fn get_proofs(&self, peer: &PeerId, io: &NetworkContext, data: UntrustedRlp) -> Result<(), Error> {
const MAX_PROOFS: usize = 128;
let peers = self.peers.read();
let peer = match peers.get(peer) {
Some(peer) => peer,
None => {
debug!(target: "les", "Ignoring request from unknown peer");
return Ok(())
}
};
let mut peer = peer.lock();
let req_id: u64 = try!(data.val_at(0));
let req = {
let requests: Result<Vec<_>, Error> = data.iter().skip(1).take(MAX_PROOFS).map(|x| {
Ok(request::StateProof {
block: try!(x.val_at(0)),
key1: try!(x.val_at(1)),
key2: if try!(x.at(2)).is_empty() { None } else { Some(try!(x.val_at(2))) },
from_level: try!(x.val_at(3)),
})
}).collect();
request::StateProofs {
requests: try!(requests),
}
};
let max_cost = try!(peer.deduct_max(&self.flow_params, request::Kind::StateProofs, req.requests.len()));
let response = self.provider.proofs(req);
let response_len = response.iter().filter(|x| &x[..] != &::rlp::EMPTY_LIST_RLP).count();
let actual_cost = self.flow_params.compute_cost(request::Kind::StateProofs, response_len);
assert!(max_cost >= actual_cost, "Actual cost exceeded maximum computed cost.");
let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost);
io.respond(packet::PROOFS, {
let mut stream = RlpStream::new_list(response.len() + 2);
stream.append(&req_id).append(&cur_buffer);
for proof in response {
stream.append_raw(&proof, 1);
}
stream.out()
}).map_err(Into::into)
}
// Receive a response for proofs.
@ -408,8 +625,53 @@ impl LightProtocol {
}
// Handle a request for contract code.
fn get_contract_code(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> {
unimplemented!()
fn get_contract_code(&self, peer: &PeerId, io: &NetworkContext, data: UntrustedRlp) -> Result<(), Error> {
const MAX_CODES: usize = 256;
let peers = self.peers.read();
let peer = match peers.get(peer) {
Some(peer) => peer,
None => {
debug!(target: "les", "Ignoring request from unknown peer");
return Ok(())
}
};
let mut peer = peer.lock();
let req_id: u64 = try!(data.val_at(0));
let req = {
let requests: Result<Vec<_>, Error> = data.iter().skip(1).take(MAX_CODES).map(|x| {
Ok(request::ContractCode {
block_hash: try!(x.val_at(0)),
account_key: try!(x.val_at(1)),
})
}).collect();
request::ContractCodes {
code_requests: try!(requests),
}
};
let max_cost = try!(peer.deduct_max(&self.flow_params, request::Kind::Codes, req.code_requests.len()));
let response = self.provider.contract_code(req);
let response_len = response.iter().filter(|x| !x.is_empty()).count();
let actual_cost = self.flow_params.compute_cost(request::Kind::Codes, response_len);
assert!(max_cost >= actual_cost, "Actual cost exceeded maximum computed cost.");
let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost);
io.respond(packet::CONTRACT_CODES, {
let mut stream = RlpStream::new_list(response.len() + 2);
stream.append(&req_id).append(&cur_buffer);
for code in response {
stream.append_raw(&code, 1);
}
stream.out()
}).map_err(Into::into)
}
// Receive a response for contract code.
@ -418,8 +680,54 @@ impl LightProtocol {
}
// Handle a request for header proofs
fn get_header_proofs(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> {
unimplemented!()
fn get_header_proofs(&self, peer: &PeerId, io: &NetworkContext, data: UntrustedRlp) -> Result<(), Error> {
const MAX_PROOFS: usize = 256;
let peers = self.peers.read();
let peer = match peers.get(peer) {
Some(peer) => peer,
None => {
debug!(target: "les", "Ignoring request from unknown peer");
return Ok(())
}
};
let mut peer = peer.lock();
let req_id: u64 = try!(data.val_at(0));
let req = {
let requests: Result<Vec<_>, Error> = data.iter().skip(1).take(MAX_PROOFS).map(|x| {
Ok(request::HeaderProof {
cht_number: try!(x.val_at(0)),
block_number: try!(x.val_at(1)),
from_level: try!(x.val_at(2)),
})
}).collect();
request::HeaderProofs {
requests: try!(requests),
}
};
let max_cost = try!(peer.deduct_max(&self.flow_params, request::Kind::HeaderProofs, req.requests.len()));
let response = self.provider.header_proofs(req);
let response_len = response.iter().filter(|x| &x[..] != ::rlp::EMPTY_LIST_RLP).count();
let actual_cost = self.flow_params.compute_cost(request::Kind::HeaderProofs, response_len);
assert!(max_cost >= actual_cost, "Actual cost exceeded maximum computed cost.");
let cur_buffer = peer.refund(&self.flow_params, max_cost - actual_cost);
io.respond(packet::HEADER_PROOFS, {
let mut stream = RlpStream::new_list(response.len() + 2);
stream.append(&req_id).append(&cur_buffer);
for proof in response {
stream.append_raw(&proof, 1);
}
stream.out()
}).map_err(Into::into)
}
// Receive a response for header proofs
@ -428,8 +736,18 @@ impl LightProtocol {
}
// Receive a set of transactions to relay.
fn relay_transactions(&self, _: &PeerId, _: &NetworkContext, _: UntrustedRlp) -> Result<(), Error> {
unimplemented!()
fn relay_transactions(&self, peer: &PeerId, data: UntrustedRlp) -> Result<(), Error> {
const MAX_TRANSACTIONS: usize = 256;
let txs: Vec<_> = try!(data.iter().take(MAX_TRANSACTIONS).map(|x| x.as_val::<SignedTransaction>()).collect());
debug!(target: "les", "Received {} transactions to relay from peer {}", txs.len(), peer);
for handler in &self.handlers {
handler.on_transactions(*peer, &txs);
}
Ok(())
}
}
@ -464,7 +782,7 @@ impl NetworkProtocolHandler for LightProtocol {
packet::GET_HEADER_PROOFS => self.get_header_proofs(peer, io, rlp),
packet::HEADER_PROOFS => self.header_proofs(peer, io, rlp),
packet::SEND_TRANSACTIONS => self.relay_transactions(peer, io, rlp),
packet::SEND_TRANSACTIONS => self.relay_transactions(peer, rlp),
other => {
Err(Error::UnrecognizedPacket(other))
@ -504,3 +822,85 @@ impl NetworkProtocolHandler for LightProtocol {
}
}
}
// Helper for encoding the request to RLP with the given ID.
fn encode_request(req: &Request, req_id: usize) -> Vec<u8> {
match *req {
Request::Headers(ref headers) => {
let mut stream = RlpStream::new_list(5);
stream
.append(&req_id)
.begin_list(2)
.append(&headers.block_num)
.append(&headers.block_hash)
.append(&headers.max)
.append(&headers.skip)
.append(&headers.reverse);
stream.out()
}
Request::Bodies(ref request) => {
let mut stream = RlpStream::new_list(request.block_hashes.len() + 1);
stream.append(&req_id);
for hash in &request.block_hashes {
stream.append(hash);
}
stream.out()
}
Request::Receipts(ref request) => {
let mut stream = RlpStream::new_list(request.block_hashes.len() + 1);
stream.append(&req_id);
for hash in &request.block_hashes {
stream.append(hash);
}
stream.out()
}
Request::StateProofs(ref request) => {
let mut stream = RlpStream::new_list(request.requests.len() + 1);
stream.append(&req_id);
for proof_req in &request.requests {
stream.begin_list(4)
.append(&proof_req.block)
.append(&proof_req.key1);
match proof_req.key2 {
Some(ref key2) => stream.append(key2),
None => stream.append_empty_data(),
};
stream.append(&proof_req.from_level);
}
stream.out()
}
Request::Codes(ref request) => {
let mut stream = RlpStream::new_list(request.code_requests.len() + 1);
stream.append(&req_id);
for code_req in &request.code_requests {
stream.begin_list(2)
.append(&code_req.block_hash)
.append(&code_req.account_key);
}
stream.out()
}
Request::HeaderProofs(ref request) => {
let mut stream = RlpStream::new_list(request.requests.len() + 1);
stream.append(&req_id);
for proof_req in &request.requests {
stream.begin_list(3)
.append(&proof_req.cht_number)
.append(&proof_req.block_number)
.append(&proof_req.from_level);
}
stream.out()
}
}
}

View File

@ -183,8 +183,10 @@ pub struct Capabilities {
/// Whether this peer can serve headers
pub serve_headers: bool,
/// Earliest block number it can serve block/receipt requests for.
/// `None` means no requests will be servable.
pub serve_chain_since: Option<u64>,
/// Earliest block number it can serve state requests for.
/// `None` means no requests will be servable.
pub serve_state_since: Option<u64>,
/// Whether it can relay transactions to the eth network.
pub tx_relay: bool,
@ -201,6 +203,16 @@ impl Default for Capabilities {
}
}
impl Capabilities {
/// Update the capabilities from an announcement.
pub fn update_from(&mut self, announcement: &Announcement) {
self.serve_headers = self.serve_headers || announcement.serve_headers;
self.serve_state_since = self.serve_state_since.or(announcement.serve_state_since);
self.serve_chain_since = self.serve_chain_since.or(announcement.serve_chain_since);
self.tx_relay = self.tx_relay || announcement.tx_relay;
}
}
/// Attempt to parse a handshake message into its three parts:
/// - chain status
/// - serving capabilities

View File

@ -17,8 +17,11 @@
//! A provider for the LES protocol. This is typically a full node, who can
//! give as much data as necessary to its peers.
use ethcore::transaction::SignedTransaction;
use ethcore::blockchain_info::BlockChainInfo;
use ethcore::client::{BlockChainClient, ProvingBlockChainClient};
use ethcore::transaction::SignedTransaction;
use ethcore::ids::BlockID;
use util::{Bytes, H256};
use request;
@ -26,7 +29,8 @@ use request;
/// Defines the operations that a provider for `LES` must fulfill.
///
/// These are defined at [1], but may be subject to change.
/// Requests which can't be fulfilled should return an empty RLP list.
/// Requests which can't be fulfilled should return either an empty RLP list
/// or empty vector where appropriate.
///
/// [1]: https://github.com/ethcore/parity/wiki/Light-Ethereum-Subprotocol-(LES)
pub trait Provider: Send + Sync {
@ -34,9 +38,12 @@ pub trait Provider: Send + Sync {
fn chain_info(&self) -> BlockChainInfo;
/// Find the depth of a common ancestor between two blocks.
/// If either block is unknown or an ancestor can't be found
/// then return `None`.
fn reorg_depth(&self, a: &H256, b: &H256) -> Option<u64>;
/// Earliest state.
/// Earliest block where state queries are available.
/// If `None`, no state queries are servable.
fn earliest_state(&self) -> Option<u64>;
/// Provide a list of headers starting at the requested block,
@ -57,11 +64,12 @@ pub trait Provider: Send + Sync {
/// Provide a set of merkle proofs, as requested. Each request is a
/// block hash and request parameters.
///
/// Returns a vector to RLP-encoded lists satisfying the requests.
/// Returns a vector of RLP-encoded lists satisfying the requests.
fn proofs(&self, req: request::StateProofs) -> Vec<Bytes>;
/// Provide contract code for the specified (block_hash, account_hash) pairs.
fn code(&self, req: request::ContractCodes) -> Vec<Bytes>;
/// Each item in the resulting vector is either the raw bytecode or empty.
fn contract_code(&self, req: request::ContractCodes) -> Vec<Bytes>;
/// Provide header proofs from the Canonical Hash Tries.
fn header_proofs(&self, req: request::HeaderProofs) -> Vec<Bytes>;
@ -69,3 +77,92 @@ pub trait Provider: Send + Sync {
/// Provide pending transactions.
fn pending_transactions(&self) -> Vec<SignedTransaction>;
}
// Implementation of a light client data provider for a client.
impl<T: ProvingBlockChainClient + ?Sized> Provider for T {
fn chain_info(&self) -> BlockChainInfo {
BlockChainClient::chain_info(self)
}
fn reorg_depth(&self, a: &H256, b: &H256) -> Option<u64> {
self.tree_route(a, b).map(|route| route.index as u64)
}
fn earliest_state(&self) -> Option<u64> {
Some(self.pruning_info().earliest_state)
}
fn block_headers(&self, req: request::Headers) -> Vec<Bytes> {
let best_num = self.chain_info().best_block_number;
let start_num = req.block_num;
match self.block_hash(BlockID::Number(req.block_num)) {
Some(hash) if hash == req.block_hash => {}
_=> {
trace!(target: "les_provider", "unknown/non-canonical start block in header request: {:?}", (req.block_num, req.block_hash));
return vec![]
}
}
(0u64..req.max as u64)
.map(|x: u64| x.saturating_mul(req.skip))
.take_while(|x| if req.reverse { x < &start_num } else { best_num - start_num < *x })
.map(|x| if req.reverse { start_num - x } else { start_num + x })
.map(|x| self.block_header(BlockID::Number(x)))
.take_while(|x| x.is_some())
.flat_map(|x| x)
.collect()
}
fn block_bodies(&self, req: request::Bodies) -> Vec<Bytes> {
req.block_hashes.into_iter()
.map(|hash| self.block_body(BlockID::Hash(hash)))
.map(|body| body.unwrap_or_else(|| ::rlp::EMPTY_LIST_RLP.to_vec()))
.collect()
}
fn receipts(&self, req: request::Receipts) -> Vec<Bytes> {
req.block_hashes.into_iter()
.map(|hash| self.block_receipts(&hash))
.map(|receipts| receipts.unwrap_or_else(|| ::rlp::EMPTY_LIST_RLP.to_vec()))
.collect()
}
fn proofs(&self, req: request::StateProofs) -> Vec<Bytes> {
use rlp::{RlpStream, Stream};
let mut results = Vec::with_capacity(req.requests.len());
for request in req.requests {
let proof = match request.key2 {
Some(key2) => self.prove_storage(request.key1, key2, request.from_level, BlockID::Hash(request.block)),
None => self.prove_account(request.key1, request.from_level, BlockID::Hash(request.block)),
};
let mut stream = RlpStream::new_list(proof.len());
for node in proof {
stream.append_raw(&node, 1);
}
results.push(stream.out());
}
results
}
fn contract_code(&self, req: request::ContractCodes) -> Vec<Bytes> {
req.code_requests.into_iter()
.map(|req| {
self.code_by_hash(req.account_key, BlockID::Hash(req.block_hash))
})
.collect()
}
fn header_proofs(&self, req: request::HeaderProofs) -> Vec<Bytes> {
req.requests.into_iter().map(|_| ::rlp::EMPTY_LIST_RLP.to_vec()).collect()
}
fn pending_transactions(&self) -> Vec<SignedTransaction> {
BlockChainClient::pending_transactions(self)
}
}

View File

@ -16,25 +16,26 @@
//! LES request types.
// TODO: make IPC compatible.
use util::H256;
/// A request for block headers.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Binary)]
pub struct Headers {
/// Block information for the request being made.
pub block: (u64, H256),
/// Starting block number
pub block_num: u64,
/// Starting block hash. This and number could be combined but IPC codegen is
/// not robust enough to support it.
pub block_hash: H256,
/// The maximum amount of headers which can be returned.
pub max: usize,
/// The amount of headers to skip between each response entry.
pub skip: usize,
pub skip: u64,
/// Whether the headers should proceed in falling number from the initial block.
pub reverse: bool,
}
/// A request for specific block bodies.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Binary)]
pub struct Bodies {
/// Hashes which bodies are being requested for.
pub block_hashes: Vec<H256>
@ -44,14 +45,14 @@ pub struct Bodies {
///
/// This request is answered with a list of transaction receipts for each block
/// requested.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Binary)]
pub struct Receipts {
/// Block hashes to return receipts for.
pub block_hashes: Vec<H256>,
}
/// A request for a state proof
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Binary)]
pub struct StateProof {
/// Block hash to query state from.
pub block: H256,
@ -65,21 +66,30 @@ pub struct StateProof {
}
/// A request for state proofs.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Binary)]
pub struct StateProofs {
/// All the proof requests.
pub requests: Vec<StateProof>,
}
/// A request for contract code.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Binary)]
pub struct ContractCode {
/// Block hash
pub block_hash: H256,
/// Account key (== sha3(address))
pub account_key: H256,
}
/// A request for contract code.
#[derive(Debug, Clone, PartialEq, Eq, Binary)]
pub struct ContractCodes {
/// Block hash and account key (== sha3(address)) pairs to fetch code for.
pub code_requests: Vec<(H256, H256)>,
pub code_requests: Vec<ContractCode>,
}
/// A request for a header proof from the Canonical Hash Trie.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Binary)]
pub struct HeaderProof {
/// Number of the CHT.
pub cht_number: u64,
@ -90,14 +100,14 @@ pub struct HeaderProof {
}
/// A request for header proofs from the CHT.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Binary)]
pub struct HeaderProofs {
/// All the proof requests.
pub requests: Vec<HeaderProofs>,
pub requests: Vec<HeaderProof>,
}
/// Kinds of requests.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Binary)]
pub enum Kind {
/// Requesting headers.
Headers,
@ -114,7 +124,7 @@ pub enum Kind {
}
/// Encompasses all possible types of requests in a single structure.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Binary)]
pub enum Request {
/// Requesting headers.
Headers(Headers),
@ -142,4 +152,16 @@ impl Request {
Request::HeaderProofs(_) => Kind::HeaderProofs,
}
}
/// Get the amount of requests being made.
pub fn amount(&self) -> usize {
match *self {
Request::Headers(ref req) => req.max,
Request::Bodies(ref req) => req.block_hashes.len(),
Request::Receipts(ref req) => req.block_hashes.len(),
Request::StateProofs(ref req) => req.requests.len(),
Request::Codes(ref req) => req.code_requests.len(),
Request::HeaderProofs(ref req) => req.requests.len(),
}
}
}

View File

@ -0,0 +1,20 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! Types used in the public (IPC) api which require custom code generation.
#![allow(dead_code, unused_assignments, unused_variables)] // codegen issues
include!(concat!(env!("OUT_DIR"), "/mod.rs.in"));

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
pub mod les_request;

View File

@ -194,6 +194,11 @@ impl AccountProvider {
Ok(self.address_book.write().set_meta(account, meta))
}
/// Removes and address from the addressbook
pub fn remove_address(&self, addr: Address) -> Result<(), Error> {
Ok(self.address_book.write().remove(addr))
}
/// Returns each account along with name and meta.
pub fn accounts_info(&self) -> Result<HashMap<Address, AccountMeta>, Error> {
let r: HashMap<Address, AccountMeta> = try!(self.sstore.accounts())

View File

@ -74,6 +74,12 @@ impl AddressBook {
}
self.save();
}
/// Removes an entry
pub fn remove(&mut self, a: Address) {
self.cache.remove(&a);
self.save();
}
}
/// Dapps user settings
@ -244,4 +250,22 @@ mod tests {
}
]);
}
#[test]
fn should_remove_address() {
let temp = RandomTempPath::create_dir();
let path = temp.as_str().to_owned();
let mut b = AddressBook::new(path.clone());
b.set_name(1.into(), "One".to_owned());
b.set_name(2.into(), "Two".to_owned());
b.set_name(3.into(), "Three".to_owned());
b.remove(2.into());
let b = AddressBook::new(path);
assert_eq!(b.get(), hash_map![
1.into() => AccountMeta{name: "One".to_owned(), meta: "{}".to_owned(), uuid: None},
3.into() => AccountMeta{name: "Three".to_owned(), meta: "{}".to_owned(), uuid: None}
]);
}
}

View File

@ -34,6 +34,7 @@ use blockchain::update::ExtrasUpdate;
use blockchain::{CacheSize, ImportRoute, Config};
use db::{self, Writable, Readable, CacheUpdatePolicy};
use cache_manager::CacheManager;
use engines::Engine;
const LOG_BLOOMS_LEVELS: usize = 3;
const LOG_BLOOMS_ELEMENTS_PER_INDEX: usize = 16;
@ -198,6 +199,9 @@ pub struct BlockChain {
pending_block_hashes: RwLock<HashMap<BlockNumber, H256>>,
pending_block_details: RwLock<HashMap<H256, BlockDetails>>,
pending_transaction_addresses: RwLock<HashMap<H256, Option<TransactionAddress>>>,
// Used for block ordering.
engine: Arc<Engine>,
}
impl BlockProvider for BlockChain {
@ -415,9 +419,8 @@ impl<'a> Iterator for AncestryIter<'a> {
}
impl BlockChain {
#[cfg_attr(feature="dev", allow(useless_let_if_seq))]
/// Create new instance of blockchain from given Genesis
pub fn new(config: Config, genesis: &[u8], db: Arc<Database>) -> BlockChain {
/// Create new instance of blockchain from given Genesis and block picking rules of Engine.
pub fn new(config: Config, genesis: &[u8], db: Arc<Database>, engine: Arc<Engine>) -> BlockChain {
// 400 is the avarage size of the key
let cache_man = CacheManager::new(config.pref_cache_size, config.max_cache_size, 400);
@ -442,6 +445,7 @@ impl BlockChain {
pending_block_hashes: RwLock::new(HashMap::new()),
pending_block_details: RwLock::new(HashMap::new()),
pending_transaction_addresses: RwLock::new(HashMap::new()),
engine: engine,
};
// load best block
@ -858,13 +862,12 @@ impl BlockChain {
let number = header.number();
let parent_hash = header.parent_hash();
let parent_details = self.block_details(&parent_hash).unwrap_or_else(|| panic!("Invalid parent hash: {:?}", parent_hash));
let total_difficulty = parent_details.total_difficulty + header.difficulty();
let is_new_best = total_difficulty > self.best_block_total_difficulty();
let is_new_best = self.engine.is_new_best_block(self.best_block_total_difficulty(), HeaderView::new(&self.best_block_header()), &parent_details, header);
BlockInfo {
hash: hash,
number: number,
total_difficulty: total_difficulty,
total_difficulty: parent_details.total_difficulty + header.difficulty(),
location: if is_new_best {
// on new best block we need to make sure that all ancestors
// are moved to "canon chain"
@ -1319,11 +1322,16 @@ mod tests {
use views::BlockView;
use transaction::{Transaction, Action};
use log_entry::{LogEntry, LocalizedLogEntry};
use spec::Spec;
fn new_db(path: &str) -> Arc<Database> {
Arc::new(Database::open(&DatabaseConfig::with_columns(::db::NUM_COLUMNS), path).unwrap())
}
fn new_chain(genesis: &[u8], db: Arc<Database>) -> BlockChain {
BlockChain::new(Config::default(), genesis, db, Spec::new_null().engine)
}
#[test]
fn should_cache_best_block() {
// given
@ -1334,7 +1342,7 @@ mod tests {
let temp = RandomTempPath::new();
let db = new_db(temp.as_str());
let bc = BlockChain::new(Config::default(), &genesis, db.clone());
let bc = new_chain(&genesis, db.clone());
assert_eq!(bc.best_block_number(), 0);
// when
@ -1360,7 +1368,7 @@ mod tests {
let temp = RandomTempPath::new();
let db = new_db(temp.as_str());
let bc = BlockChain::new(Config::default(), &genesis, db.clone());
let bc = new_chain(&genesis, db.clone());
assert_eq!(bc.genesis_hash(), genesis_hash.clone());
assert_eq!(bc.best_block_hash(), genesis_hash.clone());
@ -1391,7 +1399,7 @@ mod tests {
let temp = RandomTempPath::new();
let db = new_db(temp.as_str());
let bc = BlockChain::new(Config::default(), &genesis, db.clone());
let bc = new_chain(&genesis, db.clone());
let mut block_hashes = vec![genesis_hash.clone()];
let mut batch = db.transaction();
@ -1427,7 +1435,7 @@ mod tests {
let temp = RandomTempPath::new();
let db = new_db(temp.as_str());
let bc = BlockChain::new(Config::default(), &genesis, db.clone());
let bc = new_chain(&genesis, db.clone());
let mut batch =db.transaction();
for b in &[&b1a, &b1b, &b2a, &b2b, &b3a, &b3b, &b4a, &b4b, &b5a, &b5b] {
@ -1489,7 +1497,7 @@ mod tests {
let temp = RandomTempPath::new();
let db = new_db(temp.as_str());
let bc = BlockChain::new(Config::default(), &genesis, db.clone());
let bc = new_chain(&genesis, db.clone());
let mut batch = db.transaction();
let _ = bc.insert_block(&mut batch, &b1a, vec![]);
@ -1577,7 +1585,7 @@ mod tests {
let temp = RandomTempPath::new();
let db = new_db(temp.as_str());
let bc = BlockChain::new(Config::default(), &genesis, db.clone());
let bc = new_chain(&genesis, db.clone());
let mut batch = db.transaction();
let _ = bc.insert_block(&mut batch, &b1a, vec![]);
@ -1639,7 +1647,7 @@ mod tests {
let temp = RandomTempPath::new();
let db = new_db(temp.as_str());
let bc = BlockChain::new(Config::default(), &genesis, db.clone());
let bc = new_chain(&genesis, db.clone());
let mut batch = db.transaction();
let ir1 = bc.insert_block(&mut batch, &b1, vec![]);
@ -1755,7 +1763,7 @@ mod tests {
let temp = RandomTempPath::new();
{
let db = new_db(temp.as_str());
let bc = BlockChain::new(Config::default(), &genesis, db.clone());
let bc = new_chain(&genesis, db.clone());
assert_eq!(bc.best_block_hash(), genesis_hash);
let mut batch =db.transaction();
bc.insert_block(&mut batch, &first, vec![]);
@ -1766,7 +1774,7 @@ mod tests {
{
let db = new_db(temp.as_str());
let bc = BlockChain::new(Config::default(), &genesis, db.clone());
let bc = new_chain(&genesis, db.clone());
assert_eq!(bc.best_block_hash(), first_hash);
}
@ -1821,7 +1829,7 @@ mod tests {
let temp = RandomTempPath::new();
let db = new_db(temp.as_str());
let bc = BlockChain::new(Config::default(), &genesis, db.clone());
let bc = new_chain(&genesis, db.clone());
let mut batch =db.transaction();
bc.insert_block(&mut batch, &b1, vec![]);
db.write(batch).unwrap();
@ -1881,7 +1889,7 @@ mod tests {
let temp = RandomTempPath::new();
let db = new_db(temp.as_str());
let bc = BlockChain::new(Config::default(), &genesis, db.clone());
let bc = new_chain(&genesis, db.clone());
insert_block(&db, &bc, &b1, vec![Receipt {
state_root: H256::default(),
gas_used: 10_000.into(),
@ -1985,7 +1993,7 @@ mod tests {
let temp = RandomTempPath::new();
let db = new_db(temp.as_str());
let bc = BlockChain::new(Config::default(), &genesis, db.clone());
let bc = new_chain(&genesis, db.clone());
let blocks_b1 = bc.blocks_with_bloom(&bloom_b1, 0, 5);
let blocks_b2 = bc.blocks_with_bloom(&bloom_b2, 0, 5);
@ -2042,7 +2050,7 @@ mod tests {
{
let db = new_db(temp.as_str());
let bc = BlockChain::new(Config::default(), &genesis, db.clone());
let bc = new_chain(&genesis, db.clone());
let uncle = canon_chain.fork(1).generate(&mut finalizer.fork()).unwrap();
let mut batch =db.transaction();
@ -2061,7 +2069,7 @@ mod tests {
// re-loading the blockchain should load the correct best block.
let db = new_db(temp.as_str());
let bc = BlockChain::new(Config::default(), &genesis, db.clone());
let bc = new_chain(&genesis, db.clone());
assert_eq!(bc.best_block_number(), 5);
}
@ -2078,7 +2086,7 @@ mod tests {
let temp = RandomTempPath::new();
let db = new_db(temp.as_str());
let bc = BlockChain::new(Config::default(), &genesis, db.clone());
let bc = new_chain(&genesis, db.clone());
let mut batch =db.transaction();
bc.insert_block(&mut batch, &first, vec![]);

View File

@ -54,7 +54,7 @@ use blockchain::{BlockChain, BlockProvider, TreeRoute, ImportRoute};
use client::{
BlockId, TransactionId, UncleId, TraceId, ClientConfig, BlockChainClient,
MiningBlockChainClient, TraceFilter, CallAnalytics, BlockImportError, Mode,
ChainNotify,
ChainNotify, PruningInfo, ProvingBlockChainClient,
};
use client::Error as ClientError;
use env_info::EnvInfo;
@ -173,7 +173,7 @@ impl Client {
let gb = spec.genesis_block();
let db = Arc::new(try!(Database::open(&db_config, &path.to_str().expect("DB path could not be converted to string.")).map_err(ClientError::Database)));
let chain = Arc::new(BlockChain::new(config.blockchain.clone(), &gb, db.clone()));
let chain = Arc::new(BlockChain::new(config.blockchain.clone(), &gb, db.clone(), spec.engine.clone()));
let tracedb = RwLock::new(TraceDB::new(config.tracing.clone(), db.clone(), chain.clone()));
let trie_spec = match config.fat_db {
@ -854,7 +854,7 @@ impl snapshot::DatabaseRestore for Client {
let cache_size = state_db.cache_size();
*state_db = StateDB::new(journaldb::new(db.clone(), self.pruning, ::db::COL_STATE), cache_size);
*chain = Arc::new(BlockChain::new(self.config.blockchain.clone(), &[], db.clone()));
*chain = Arc::new(BlockChain::new(self.config.blockchain.clone(), &[], db.clone(), self.engine.clone()));
*tracedb = TraceDB::new(self.config.tracing.clone(), db.clone(), chain.clone());
Ok(())
}
@ -1339,7 +1339,7 @@ impl BlockChainClient for Client {
self.miner.pending_transactions(self.chain.read().best_block_number())
}
fn signing_network_id(&self) -> Option<u8> {
fn signing_network_id(&self) -> Option<u64> {
self.engine.signing_network_id(&self.latest_env_info())
}
@ -1353,6 +1353,13 @@ impl BlockChainClient for Client {
self.uncle(id)
.map(|header| self.engine.extra_info(&decode(&header)))
}
fn pruning_info(&self) -> PruningInfo {
PruningInfo {
earliest_chain: self.chain.read().first_block_number().unwrap_or(1),
earliest_state: self.state_db.lock().journal_db().earliest_era().unwrap_or(0),
}
}
}
impl MiningBlockChainClient for Client {
@ -1437,13 +1444,40 @@ impl MayPanic for Client {
}
}
impl ProvingBlockChainClient for Client {
fn prove_storage(&self, key1: H256, key2: H256, from_level: u32, id: BlockID) -> Vec<Bytes> {
self.state_at(id)
.and_then(move |state| state.prove_storage(key1, key2, from_level).ok())
.unwrap_or_else(Vec::new)
}
#[test]
fn should_not_cache_details_before_commit() {
fn prove_account(&self, key1: H256, from_level: u32, id: BlockID) -> Vec<Bytes> {
self.state_at(id)
.and_then(move |state| state.prove_account(key1, from_level).ok())
.unwrap_or_else(Vec::new)
}
fn code_by_hash(&self, account_key: H256, id: BlockID) -> Bytes {
self.state_at(id)
.and_then(move |state| state.code_by_address_hash(account_key).ok())
.and_then(|x| x)
.unwrap_or_else(Vec::new)
}
}
#[cfg(test)]
mod tests {
#[test]
fn should_not_cache_details_before_commit() {
use client::BlockChainClient;
use tests::helpers::*;
use std::thread;
use std::time::Duration;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use util::kvdb::DBTransaction;
let client = generate_dummy_client(0);
let genesis = client.chain_info().best_block_hash;
@ -1465,4 +1499,5 @@ fn should_not_cache_details_before_commit() {
while !go.load(Ordering::SeqCst) { thread::park_timeout(Duration::from_millis(5)); }
assert!(client.tree_route(&genesis, &new_hash).is_none());
}
}

View File

@ -29,18 +29,21 @@ mod fetch;
pub use self::client::*;
pub use self::config::{Mode, ClientConfig, UpdatePolicy, UpdateFilter, DatabaseCompactionProfile, BlockChainConfig, VMType};
pub use self::error::Error;
pub use types::ids::*;
pub use self::test_client::{TestBlockChainClient, EachBlockWith};
pub use self::chain_notify::ChainNotify;
pub use self::traits::{BlockChainClient, MiningBlockChainClient, ProvingBlockChainClient};
pub use types::ids::*;
pub use types::trace_filter::Filter as TraceFilter;
pub use types::pruning_info::PruningInfo;
pub use types::call_analytics::CallAnalytics;
pub use executive::{Executed, Executive, TransactOptions};
pub use env_info::{LastHashes, EnvInfo};
pub use self::chain_notify::ChainNotify;
pub use types::call_analytics::CallAnalytics;
pub use block_import_error::BlockImportError;
pub use transaction_import::TransactionImportResult;
pub use transaction_import::TransactionImportError;
pub use self::traits::{BlockChainClient, MiningBlockChainClient};
pub use verification::VerifierType;
/// IPC interfaces

View File

@ -38,6 +38,7 @@ use evm::{Factory as EvmFactory, VMType, Schedule};
use miner::{Miner, MinerService, TransactionImportResult};
use spec::Spec;
use types::mode::Mode;
use types::pruning_info::PruningInfo;
use views::BlockView;
use verification::queue::QueueInfo;
@ -662,9 +663,16 @@ impl BlockChainClient for TestBlockChainClient {
self.miner.pending_transactions(self.chain_info().best_block_number)
}
fn signing_network_id(&self) -> Option<u8> { None }
fn signing_network_id(&self) -> Option<u64> { None }
fn mode(&self) -> Mode { Mode::Active }
fn set_mode(&self, _: Mode) { unimplemented!(); }
fn pruning_info(&self) -> PruningInfo {
PruningInfo {
earliest_chain: 1,
earliest_state: 1,
}
}
}

View File

@ -39,6 +39,7 @@ use types::call_analytics::CallAnalytics;
use types::blockchain_info::BlockChainInfo;
use types::block_status::BlockStatus;
use types::mode::Mode;
use types::pruning_info::PruningInfo;
#[ipc(client_ident="RemoteClient")]
/// Blockchain database client. Owns and manages a blockchain and a block queue.
@ -237,7 +238,7 @@ pub trait BlockChainClient : Sync + Send {
}
/// Get the preferred network ID to sign on
fn signing_network_id(&self) -> Option<u8>;
fn signing_network_id(&self) -> Option<u64>;
/// Get the mode.
fn mode(&self) -> Mode;
@ -248,12 +249,17 @@ pub trait BlockChainClient : Sync + Send {
/// Returns engine-related extra info for `BlockId`.
fn block_extra_info(&self, id: BlockId) -> Option<BTreeMap<String, String>>;
/// Returns engine-related extra info for `UncleId`.
/// Returns engine-related extra info for `UncleID`.
fn uncle_extra_info(&self, id: UncleId) -> Option<BTreeMap<String, String>>;
/// Returns information about pruning/data availability.
fn pruning_info(&self) -> PruningInfo;
}
impl IpcConfig for BlockChainClient { }
/// Extended client interface used for mining
pub trait MiningBlockChainClient : BlockChainClient {
pub trait MiningBlockChainClient: BlockChainClient {
/// Returns OpenBlock prepared for closing.
fn prepare_open_block(&self,
author: Address,
@ -271,4 +277,23 @@ pub trait MiningBlockChainClient : BlockChainClient {
fn latest_schedule(&self) -> Schedule;
}
impl IpcConfig for BlockChainClient { }
/// Extended client interface for providing proofs of the state.
pub trait ProvingBlockChainClient: BlockChainClient {
/// Prove account storage at a specific block id.
///
/// Both provided keys assume a secure trie.
/// Returns a vector of raw trie nodes (in order from the root) proving the storage query.
/// Nodes after `from_level` may be omitted.
/// An empty vector indicates unservable query.
fn prove_storage(&self, key1: H256, key2: H256, from_level: u32, id: BlockID) -> Vec<Bytes>;
/// Prove account existence at a specific block id.
/// The key is the keccak hash of the account's address.
/// Returns a vector of raw trie nodes (in order from the root) proving the query.
/// Nodes after `from_level` may be omitted.
/// An empty vector indicates unservable query.
fn prove_account(&self, key1: H256, from_level: u32, id: BlockID) -> Vec<Bytes>;
/// Get code by address hash.
fn code_by_hash(&self, account_key: H256, id: BlockID) -> Bytes;
}

View File

@ -21,7 +21,7 @@ use std::sync::Weak;
use std::time::{UNIX_EPOCH, Duration};
use util::*;
use ethkey::{verify_address, Signature};
use rlp::{UntrustedRlp, View, encode};
use rlp::{Rlp, UntrustedRlp, View, encode};
use account_provider::AccountProvider;
use block::*;
use spec::CommonParams;
@ -35,6 +35,8 @@ use service::ClientIoMessage;
use transaction::SignedTransaction;
use env_info::EnvInfo;
use builtin::Builtin;
use blockchain::extras::BlockDetails;
use views::HeaderView;
/// `AuthorityRound` params.
#[derive(Debug, PartialEq)]
@ -272,7 +274,6 @@ impl Engine for AuthorityRound {
}
fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
// Don't calculate difficulty for genesis blocks.
if header.number() == 0 {
return Err(From::from(BlockError::RidiculousNumber(OutOfBounds { min: Some(1), max: None, found: header.number() })));
}
@ -284,10 +285,6 @@ impl Engine for AuthorityRound {
try!(Err(BlockError::DoubleVote(header.author().clone())));
}
// Check difficulty is correct given the two timestamps.
if header.difficulty() != parent.difficulty() {
return Err(From::from(BlockError::InvalidDifficulty(Mismatch { expected: *parent.difficulty(), found: *header.difficulty() })))
}
let gas_limit_divisor = self.our_params.gas_limit_bound_divisor;
let min_gas = parent.gas_limit().clone() - parent.gas_limit().clone() / gas_limit_divisor;
let max_gas = parent.gas_limit().clone() + parent.gas_limit().clone() / gas_limit_divisor;
@ -310,6 +307,19 @@ impl Engine for AuthorityRound {
let mut guard = self.message_channel.lock();
*guard = Some(message_channel);
}
fn is_new_best_block(&self, _best_total_difficulty: U256, best_header: HeaderView, _parent_details: &BlockDetails, new_header: &HeaderView) -> bool {
let new_number = new_header.number();
let best_number = best_header.number();
if new_number != best_number {
new_number > best_number
} else {
// Take the oldest step at given height.
let new_step: usize = Rlp::new(&new_header.seal()[0]).as_val();
let best_step: usize = Rlp::new(&best_header.seal()[0]).as_val();
new_step < best_step
}
}
}
#[cfg(test)]

View File

@ -38,6 +38,9 @@ use io::IoChannel;
use service::ClientIoMessage;
use header::Header;
use transaction::SignedTransaction;
use ethereum::ethash;
use blockchain::extras::BlockDetails;
use views::HeaderView;
/// A consensus mechanism for the chain. Generally either proof-of-work or proof-of-stake-based.
/// Provides hooks into each of the major parts of block import.
@ -113,7 +116,7 @@ pub trait Engine : Sync + Send {
fn verify_transaction(&self, _t: &SignedTransaction, _header: &Header) -> Result<(), Error> { Ok(()) }
/// The network ID that transactions should be signed with.
fn signing_network_id(&self, _env_info: &EnvInfo) -> Option<u8> { None }
fn signing_network_id(&self, _env_info: &EnvInfo) -> Option<u64> { None }
/// Verify the seal of a block. This is an auxilliary method that actually just calls other `verify_` methods
/// to get the job done. By default it must pass `verify_basic` and `verify_block_unordered`. If more or fewer
@ -146,5 +149,9 @@ pub trait Engine : Sync + Send {
/// Add a channel for communication with Client which can be used for sealing.
fn register_message_channel(&self, _message_channel: IoChannel<ClientIoMessage>) {}
// TODO: sealing stuff - though might want to leave this for later.
/// Check if new block should be chosen as the one in chain.
fn is_new_best_block(&self, best_total_difficulty: U256, _best_header: HeaderView, parent_details: &BlockDetails, new_header: &HeaderView) -> bool {
ethash::is_new_best_block(best_total_difficulty, parent_details, new_header)
}
}

View File

@ -38,6 +38,12 @@ impl NullEngine {
}
}
impl Default for NullEngine {
fn default() -> Self {
Self::new(Default::default(), Default::default())
}
}
impl Engine for NullEngine {
fn name(&self) -> &str {
"NullEngine"

View File

@ -21,6 +21,7 @@ use builtin::Builtin;
use env_info::EnvInfo;
use error::{BlockError, TransactionError, Error};
use header::Header;
use views::HeaderView;
use state::CleanupMode;
use spec::CommonParams;
use transaction::SignedTransaction;
@ -28,6 +29,7 @@ use engines::Engine;
use evm::Schedule;
use ethjson;
use rlp::{self, UntrustedRlp, View};
use blockchain::extras::BlockDetails;
/// Ethash params.
#[derive(Debug, PartialEq)]
@ -163,9 +165,9 @@ impl Engine for Ethash {
}
}
fn signing_network_id(&self, env_info: &EnvInfo) -> Option<u8> {
if env_info.number >= self.ethash_params.eip155_transition && self.params().network_id < 127 {
Some(self.params().network_id as u8)
fn signing_network_id(&self, env_info: &EnvInfo) -> Option<u64> {
if env_info.number >= self.ethash_params.eip155_transition {
Some(self.params().network_id)
} else {
None
}
@ -314,7 +316,7 @@ impl Engine for Ethash {
}
if let Some(n) = t.network_id() {
if header.number() < self.ethash_params.eip155_transition || n as usize != self.params().network_id {
if header.number() < self.ethash_params.eip155_transition || n != self.params().network_id {
return Err(TransactionError::InvalidNetworkId.into())
}
}
@ -325,6 +327,15 @@ impl Engine for Ethash {
fn verify_transaction(&self, t: &SignedTransaction, _header: &Header) -> Result<(), Error> {
t.sender().map(|_|()) // Perform EC recovery and cache sender
}
fn is_new_best_block(&self, best_total_difficulty: U256, _best_header: HeaderView, parent_details: &BlockDetails, new_header: &HeaderView) -> bool {
is_new_best_block(best_total_difficulty, parent_details, new_header)
}
}
/// Check if a new block should replace the best blockchain block.
pub fn is_new_best_block(best_total_difficulty: U256, parent_details: &BlockDetails, new_header: &HeaderView) -> bool {
parent_details.total_difficulty + new_header.difficulty() > best_total_difficulty
}
#[cfg_attr(feature="dev", allow(wrong_self_convention))]

View File

@ -81,6 +81,7 @@ struct Restoration {
struct RestorationParams<'a> {
manifest: ManifestData, // manifest to base restoration on.
pruning: Algorithm, // pruning algorithm for the database.
engine: Arc<Engine>, // consensus engine of the chain.
db_path: PathBuf, // database path
db_config: &'a DatabaseConfig, // configuration for the database.
writer: Option<LooseWriter>, // writer for recovered snapshot.
@ -99,7 +100,7 @@ impl Restoration {
let raw_db = Arc::new(try!(Database::open(params.db_config, &*params.db_path.to_string_lossy())
.map_err(UtilError::SimpleString)));
let chain = BlockChain::new(Default::default(), params.genesis, raw_db.clone());
let chain = BlockChain::new(Default::default(), params.genesis, raw_db.clone(), params.engine);
let blocks = try!(BlockRebuilder::new(chain, raw_db.clone(), &manifest));
let root = manifest.state_root.clone();
@ -420,6 +421,7 @@ impl Service {
let params = RestorationParams {
manifest: manifest,
pruning: self.pruning,
engine: self.engine.clone(),
db_path: self.restoration_db(),
db_config: &self.db_config,
writer: writer,

View File

@ -37,13 +37,14 @@ fn chunk_and_restore(amount: u64) {
let genesis = canon_chain.generate(&mut finalizer).unwrap();
let db_cfg = DatabaseConfig::with_columns(::db::NUM_COLUMNS);
let engine = Arc::new(::engines::NullEngine::default());
let orig_path = RandomTempPath::create_dir();
let new_path = RandomTempPath::create_dir();
let mut snapshot_path = new_path.as_path().to_owned();
snapshot_path.push("SNAP");
let old_db = Arc::new(Database::open(&db_cfg, orig_path.as_str()).unwrap());
let bc = BlockChain::new(Default::default(), &genesis, old_db.clone());
let bc = BlockChain::new(Default::default(), &genesis, old_db.clone(), engine.clone());
// build the blockchain.
let mut batch = old_db.transaction();
@ -73,21 +74,20 @@ fn chunk_and_restore(amount: u64) {
// restore it.
let new_db = Arc::new(Database::open(&db_cfg, new_path.as_str()).unwrap());
let new_chain = BlockChain::new(Default::default(), &genesis, new_db.clone());
let new_chain = BlockChain::new(Default::default(), &genesis, new_db.clone(), engine.clone());
let mut rebuilder = BlockRebuilder::new(new_chain, new_db.clone(), &manifest).unwrap();
let reader = PackedReader::new(&snapshot_path).unwrap().unwrap();
let engine = ::engines::NullEngine::new(Default::default(), Default::default());
let flag = AtomicBool::new(true);
for chunk_hash in &reader.manifest().block_hashes {
let compressed = reader.chunk(*chunk_hash).unwrap();
let chunk = snappy::decompress(&compressed).unwrap();
rebuilder.feed(&chunk, &engine, &flag).unwrap();
rebuilder.feed(&chunk, engine.as_ref(), &flag).unwrap();
}
rebuilder.finalize(HashMap::new()).unwrap();
// and test it.
let new_chain = BlockChain::new(Default::default(), &genesis, new_db);
let new_chain = BlockChain::new(Default::default(), &genesis, new_db, engine);
assert_eq!(new_chain.best_block_hash(), best_hash);
}
@ -121,8 +121,8 @@ fn checks_flag() {
let db_cfg = DatabaseConfig::with_columns(::db::NUM_COLUMNS);
let db = Arc::new(Database::open(&db_cfg, path.as_str()).unwrap());
let chain = BlockChain::new(Default::default(), &genesis, db.clone());
let engine = ::engines::NullEngine::new(Default::default(), Default::default());
let engine = Arc::new(::engines::NullEngine::default());
let chain = BlockChain::new(Default::default(), &genesis, db.clone(), engine.clone());
let manifest = ::snapshot::ManifestData {
state_hashes: Vec::new(),
@ -134,7 +134,7 @@ fn checks_flag() {
let mut rebuilder = BlockRebuilder::new(chain, db.clone(), &manifest).unwrap();
match rebuilder.feed(&chunk, &engine, &AtomicBool::new(false)) {
match rebuilder.feed(&chunk, engine.as_ref(), &AtomicBool::new(false)) {
Err(Error::Snapshot(SnapshotError::RestorationAborted)) => {}
_ => panic!("Wrong result on abort flag set")
}

View File

@ -30,15 +30,14 @@ use ethjson;
use rlp::{Rlp, RlpStream, View, Stream};
/// Parameters common to all engines.
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(test, derive(Default))]
#[derive(Debug, PartialEq, Clone, Default)]
pub struct CommonParams {
/// Account start nonce.
pub account_start_nonce: U256,
/// Maximum size of extra data.
pub maximum_extra_data_size: usize,
/// Network id.
pub network_id: usize,
pub network_id: u64,
/// Main subprotocol name.
pub subprotocol_name: String,
/// Minimum gas limit.
@ -164,7 +163,7 @@ impl Spec {
pub fn nodes(&self) -> &[String] { &self.nodes }
/// Get the configured Network ID.
pub fn network_id(&self) -> usize { self.params.network_id }
pub fn network_id(&self) -> u64 { self.params.network_id }
/// Get the configured subprotocol name.
pub fn subprotocol_name(&self) -> String { self.params.subprotocol_name.clone() }

View File

@ -436,6 +436,27 @@ impl Account {
}
}
// light client storage proof.
impl Account {
/// Prove a storage key's existence or nonexistence in the account's storage
/// trie.
/// `storage_key` is the hash of the desired storage key, meaning
/// this will only work correctly under a secure trie.
/// Returns a merkle proof of the storage trie node with all nodes before `from_level`
/// omitted.
pub fn prove_storage(&self, db: &HashDB, storage_key: H256, from_level: u32) -> Result<Vec<Bytes>, Box<TrieError>> {
use util::trie::{Trie, TrieDB};
use util::trie::recorder::{Recorder, BasicRecorder as TrieRecorder};
let mut recorder = TrieRecorder::with_depth(from_level);
let trie = try!(TrieDB::new(db, &self.storage_root));
let _ = try!(trie.get_recorded(&storage_key, &mut recorder));
Ok(recorder.drain().into_iter().map(|r| r.data).collect())
}
}
impl fmt::Debug for Account {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", PodAccount::from_account(self))

View File

@ -16,7 +16,7 @@
use std::cell::{RefCell, RefMut};
use std::collections::hash_map::Entry;
use util::*;
use receipt::Receipt;
use engines::Engine;
use env_info::EnvInfo;
@ -30,6 +30,9 @@ use types::state_diff::StateDiff;
use transaction::SignedTransaction;
use state_db::StateDB;
use util::*;
use util::trie::recorder::{Recorder, BasicRecorder as TrieRecorder};
mod account;
mod substate;
@ -758,6 +761,53 @@ impl State {
}
}
// LES state proof implementations.
impl State {
/// Prove an account's existence or nonexistence in the state trie.
/// Returns a merkle proof of the account's trie node with all nodes before `from_level`
/// omitted or an encountered trie error.
/// Requires a secure trie to be used for accurate results.
/// `account_key` == sha3(address)
pub fn prove_account(&self, account_key: H256, from_level: u32) -> Result<Vec<Bytes>, Box<TrieError>> {
let mut recorder = TrieRecorder::with_depth(from_level);
let trie = try!(TrieDB::new(self.db.as_hashdb(), &self.root));
let _ = try!(trie.get_recorded(&account_key, &mut recorder));
Ok(recorder.drain().into_iter().map(|r| r.data).collect())
}
/// Prove an account's storage key's existence or nonexistence in the state.
/// Returns a merkle proof of the account's storage trie with all nodes before
/// `from_level` omitted. Requires a secure trie to be used for correctness.
/// `account_key` == sha3(address)
/// `storage_key` == sha3(key)
pub fn prove_storage(&self, account_key: H256, storage_key: H256, from_level: u32) -> Result<Vec<Bytes>, Box<TrieError>> {
// TODO: probably could look into cache somehow but it's keyed by
// address, not sha3(address).
let trie = try!(TrieDB::new(self.db.as_hashdb(), &self.root));
let acc = match try!(trie.get(&account_key)) {
Some(rlp) => Account::from_rlp(&rlp),
None => return Ok(Vec::new()),
};
let account_db = self.factories.accountdb.readonly(self.db.as_hashdb(), account_key);
acc.prove_storage(account_db.as_hashdb(), storage_key, from_level)
}
/// Get code by address hash.
/// Only works when backed by a secure trie.
pub fn code_by_address_hash(&self, account_key: H256) -> Result<Option<Bytes>, Box<TrieError>> {
let trie = try!(TrieDB::new(self.db.as_hashdb(), &self.root));
let mut acc = match try!(trie.get(&account_key)) {
Some(rlp) => Account::from_rlp(&rlp),
None => return Ok(None),
};
let account_db = self.factories.accountdb.readonly(self.db.as_hashdb(), account_key);
Ok(acc.cache_code(account_db.as_hashdb()).map(|c| (&*c).clone()))
}
}
impl fmt::Debug for State {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self.cache.borrow())

View File

@ -286,7 +286,7 @@ fn new_db(path: &str) -> Arc<Database> {
pub fn generate_dummy_blockchain(block_number: u32) -> GuardedTempResult<BlockChain> {
let temp = RandomTempPath::new();
let db = new_db(temp.as_str());
let bc = BlockChain::new(BlockChainConfig::default(), &create_unverifiable_block(0, H256::zero()), db.clone());
let bc = BlockChain::new(BlockChainConfig::default(), &create_unverifiable_block(0, H256::zero()), db.clone(), Spec::new_null().engine);
let mut batch = db.transaction();
for block_order in 1..block_number {
@ -304,7 +304,7 @@ pub fn generate_dummy_blockchain(block_number: u32) -> GuardedTempResult<BlockCh
pub fn generate_dummy_blockchain_with_extra(block_number: u32) -> GuardedTempResult<BlockChain> {
let temp = RandomTempPath::new();
let db = new_db(temp.as_str());
let bc = BlockChain::new(BlockChainConfig::default(), &create_unverifiable_block(0, H256::zero()), db.clone());
let bc = BlockChain::new(BlockChainConfig::default(), &create_unverifiable_block(0, H256::zero()), db.clone(), Spec::new_null().engine);
let mut batch = db.transaction();
@ -323,7 +323,7 @@ pub fn generate_dummy_blockchain_with_extra(block_number: u32) -> GuardedTempRes
pub fn generate_dummy_empty_blockchain() -> GuardedTempResult<BlockChain> {
let temp = RandomTempPath::new();
let db = new_db(temp.as_str());
let bc = BlockChain::new(BlockChainConfig::default(), &create_unverifiable_block(0, H256::zero()), db.clone());
let bc = BlockChain::new(BlockChainConfig::default(), &create_unverifiable_block(0, H256::zero()), db.clone(), Spec::new_null().engine);
GuardedTempResult::<BlockChain> {
_temp: temp,

View File

@ -34,3 +34,4 @@ pub mod block_import_error;
pub mod restoration_status;
pub mod snapshot_manifest;
pub mod mode;
pub mod pruning_info;

View File

@ -0,0 +1,30 @@
// 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/>.
//! Information about portions of the state and chain which the client may serve.
//!
//! Currently assumes that a client will store everything past a certain point
//! or everything. Will be extended in the future to support a definition
//! of which portions of the ancient chain and current state trie are stored as well.
/// Client pruning info. See module-level docs for more details.
#[derive(Debug, Clone, Binary)]
pub struct PruningInfo {
/// The first block which everything can be served after.
pub earliest_chain: u64,
/// The first block where state requests may be served.
pub earliest_state: u64,
}

View File

@ -72,7 +72,7 @@ pub struct Transaction {
impl Transaction {
/// Append object with a without signature into RLP stream
pub fn rlp_append_unsigned_transaction(&self, s: &mut RlpStream, network_id: Option<u8>) {
pub fn rlp_append_unsigned_transaction(&self, s: &mut RlpStream, network_id: Option<u64>) {
s.begin_list(if network_id.is_none() { 6 } else { 9 });
s.append(&self.nonce);
s.append(&self.gas_price);
@ -140,26 +140,26 @@ impl From<ethjson::transaction::Transaction> for SignedTransaction {
impl Transaction {
/// The message hash of the transaction.
pub fn hash(&self, network_id: Option<u8>) -> H256 {
pub fn hash(&self, network_id: Option<u64>) -> H256 {
let mut stream = RlpStream::new();
self.rlp_append_unsigned_transaction(&mut stream, network_id);
stream.out().sha3()
}
/// Signs the transaction as coming from `sender`.
pub fn sign(self, secret: &Secret, network_id: Option<u8>) -> SignedTransaction {
pub fn sign(self, secret: &Secret, network_id: Option<u64>) -> SignedTransaction {
let sig = ::ethkey::sign(secret, &self.hash(network_id))
.expect("data is valid and context has signing capabilities; qed");
self.with_signature(sig, network_id)
}
/// Signs the transaction with signature.
pub fn with_signature(self, sig: Signature, network_id: Option<u8>) -> SignedTransaction {
pub fn with_signature(self, sig: Signature, network_id: Option<u64>) -> SignedTransaction {
SignedTransaction {
unsigned: self,
r: sig.r().into(),
s: sig.s().into(),
v: sig.v() + if let Some(n) = network_id { 35 + n * 2 } else { 27 },
v: sig.v() as u64 + if let Some(n) = network_id { 35 + n * 2 } else { 27 },
hash: Cell::new(None),
sender: Cell::new(None),
}
@ -211,7 +211,7 @@ pub struct SignedTransaction {
unsigned: Transaction,
/// The V field of the signature; the LS bit described which half of the curve our point falls
/// in. The MS bits describe which network this transaction is for. If 27/28, its for all networks.
v: u8,
v: u64,
/// The R field of the signature; helps describe the point on the curve.
r: U256,
/// The S field of the signature; helps describe the point on the curve.
@ -302,10 +302,13 @@ impl SignedTransaction {
}
/// 0 if `v` would have been 27 under "Electrum" notation, 1 if 28 or 4 if invalid.
pub fn standard_v(&self) -> u8 { match self.v { v if v == 27 || v == 28 || v > 36 => (v - 1) % 2, _ => 4 } }
pub fn standard_v(&self) -> u8 { match self.v { v if v == 27 || v == 28 || v > 36 => ((v - 1) % 2) as u8, _ => 4 } }
/// The `v` value that appears in the RLP.
pub fn original_v(&self) -> u64 { self.v }
/// The network ID, or `None` if this is a global transaction.
pub fn network_id(&self) -> Option<u8> {
pub fn network_id(&self) -> Option<u64> {
match self.v {
v if v > 36 => Some((v - 35) / 2),
_ => None,

View File

@ -17,7 +17,7 @@
//! A queue of blocks. Sits between network or other I/O and the `BlockChain`.
//! Sorts them ready for blockchain insertion.
use std::thread::{JoinHandle, self};
use std::thread::{self, JoinHandle};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrdering};
use std::sync::{Condvar as SCondvar, Mutex as SMutex};
use util::*;
@ -53,6 +53,8 @@ pub struct Config {
/// Maximum heap memory to use.
/// When the limit is reached, is_full returns true.
pub max_mem_use: usize,
/// Settings for the number of verifiers and adaptation strategy.
pub verifier_settings: VerifierSettings,
}
impl Default for Config {
@ -60,39 +62,35 @@ impl Default for Config {
Config {
max_queue_size: 30000,
max_mem_use: 50 * 1024 * 1024,
verifier_settings: VerifierSettings::default(),
}
}
}
struct VerifierHandle {
deleting: Arc<AtomicBool>,
sleep: Arc<AtomicBool>,
thread: JoinHandle<()>,
/// Verifier settings.
#[derive(Debug, PartialEq, Clone)]
pub struct VerifierSettings {
/// Whether to scale amount of verifiers according to load.
// Todo: replace w/ strategy enum?
pub scale_verifiers: bool,
/// Beginning amount of verifiers.
pub num_verifiers: usize,
}
impl VerifierHandle {
// signal to the verifier thread that it should sleep.
fn sleep(&self) {
self.sleep.store(true, AtomicOrdering::SeqCst);
impl Default for VerifierSettings {
fn default() -> Self {
VerifierSettings {
scale_verifiers: false,
num_verifiers: MAX_VERIFIERS,
}
}
}
// signal to the verifier thread that it should wake up.
fn wake_up(&self) {
self.sleep.store(false, AtomicOrdering::SeqCst);
self.thread.thread().unpark();
}
// signal to the verifier thread that it should conclude its
// operations.
fn conclude(&self) {
self.wake_up();
self.deleting.store(true, AtomicOrdering::Release);
}
// join the verifier thread.
fn join(self) {
self.thread.join().expect("Verifier thread panicked");
}
// pool states
enum State {
// all threads with id < inner value are to work.
Work(usize),
Exit,
}
/// An item which is in the process of being verified.
@ -131,7 +129,6 @@ pub struct VerificationQueue<K: Kind> {
engine: Arc<Engine>,
more_to_verify: Arc<SCondvar>,
verification: Arc<Verification<K>>,
verifiers: Mutex<(Vec<VerifierHandle>, usize)>,
deleting: Arc<AtomicBool>,
ready_signal: Arc<QueueSignal>,
empty: Arc<SCondvar>,
@ -139,6 +136,9 @@ pub struct VerificationQueue<K: Kind> {
ticks_since_adjustment: AtomicUsize,
max_queue_size: usize,
max_mem_use: usize,
scale_verifiers: bool,
verifier_handles: Vec<JoinHandle<()>>,
state: Arc<(Mutex<State>, Condvar)>,
}
struct QueueSignal {
@ -221,43 +221,45 @@ impl<K: Kind> VerificationQueue<K> {
});
let empty = Arc::new(SCondvar::new());
let panic_handler = PanicHandler::new_in_arc();
let scale_verifiers = config.verifier_settings.scale_verifiers;
let max_verifiers = min(::num_cpus::get(), MAX_VERIFIERS);
let default_amount = max(::num_cpus::get(), 3) - 2;
let mut verifiers = Vec::with_capacity(max_verifiers);
let num_cpus = ::num_cpus::get();
let max_verifiers = min(num_cpus, MAX_VERIFIERS);
let default_amount = max(1, min(max_verifiers, config.verifier_settings.num_verifiers));
let state = Arc::new((Mutex::new(State::Work(default_amount)), Condvar::new()));
let mut verifier_handles = Vec::with_capacity(max_verifiers);
debug!(target: "verification", "Allocating {} verifiers, {} initially active", max_verifiers, default_amount);
debug!(target: "verification", "Verifier auto-scaling {}", if scale_verifiers { "enabled" } else { "disabled" });
for i in 0..max_verifiers {
debug!(target: "verification", "Adding verification thread #{}", i);
let deleting = deleting.clone();
let panic_handler = panic_handler.clone();
let verification = verification.clone();
let engine = engine.clone();
let wait = more_to_verify.clone();
let ready = ready_signal.clone();
let empty = empty.clone();
let state = state.clone();
// enable only the first few verifiers.
let sleep = if i < default_amount {
Arc::new(AtomicBool::new(false))
} else {
Arc::new(AtomicBool::new(true))
};
verifiers.push(VerifierHandle {
deleting: deleting.clone(),
sleep: sleep.clone(),
thread: thread::Builder::new()
let handle = thread::Builder::new()
.name(format!("Verifier #{}", i))
.spawn(move || {
panic_handler.catch_panic(move || {
VerificationQueue::verify(verification, engine, wait, ready, deleting, empty, sleep)
VerificationQueue::verify(
verification,
engine,
wait,
ready,
empty,
state,
i,
)
}).unwrap()
})
.expect("Failed to create verifier thread.")
});
.expect("Failed to create verifier thread.");
verifier_handles.push(handle);
}
VerificationQueue {
@ -266,13 +268,15 @@ impl<K: Kind> VerificationQueue<K> {
ready_signal: ready_signal,
more_to_verify: more_to_verify,
verification: verification,
verifiers: Mutex::new((verifiers, default_amount)),
deleting: deleting,
processing: RwLock::new(HashSet::new()),
empty: empty,
ticks_since_adjustment: AtomicUsize::new(0),
max_queue_size: max(config.max_queue_size, MIN_QUEUE_LIMIT),
max_mem_use: max(config.max_mem_use, MIN_MEM_LIMIT),
scale_verifiers: scale_verifiers,
verifier_handles: verifier_handles,
state: state,
}
}
@ -281,23 +285,30 @@ impl<K: Kind> VerificationQueue<K> {
engine: Arc<Engine>,
wait: Arc<SCondvar>,
ready: Arc<QueueSignal>,
deleting: Arc<AtomicBool>,
empty: Arc<SCondvar>,
sleep: Arc<AtomicBool>,
state: Arc<(Mutex<State>, Condvar)>,
id: usize,
) {
while !deleting.load(AtomicOrdering::Acquire) {
loop {
// check current state.
{
while sleep.load(AtomicOrdering::SeqCst) {
trace!(target: "verification", "Verifier sleeping");
::std::thread::park();
trace!(target: "verification", "Verifier waking up");
let mut cur_state = state.0.lock();
while let State::Work(x) = *cur_state {
// sleep until this thread is required.
if id < x { break }
if deleting.load(AtomicOrdering::Acquire) {
return;
debug!(target: "verification", "verifier {} sleeping", id);
state.1.wait(&mut cur_state);
debug!(target: "verification", "verifier {} waking up", id);
}
if let State::Exit = *cur_state {
debug!(target: "verification", "verifier {} exiting", id);
break;
}
}
// wait for work if empty.
{
let mut more_to_verify = verification.more_to_verify.lock().unwrap();
@ -305,15 +316,22 @@ impl<K: Kind> VerificationQueue<K> {
empty.notify_all();
}
while verification.unverified.lock().is_empty() && !deleting.load(AtomicOrdering::Acquire) {
while verification.unverified.lock().is_empty() {
if let State::Exit = *state.0.lock() {
debug!(target: "verification", "verifier {} exiting", id);
return;
}
more_to_verify = wait.wait(more_to_verify).unwrap();
}
if deleting.load(AtomicOrdering::Acquire) {
if let State::Exit = *state.0.lock() {
debug!(target: "verification", "verifier {} exiting", id);
return;
}
}
// do work.
let item = {
// acquire these locks before getting the item to verify.
let mut unverified = verification.unverified.lock();
@ -568,6 +586,14 @@ impl<K: Kind> VerificationQueue<K> {
}
}
/// Get the current number of working verifiers.
pub fn num_verifiers(&self) -> usize {
match *self.state.0.lock() {
State::Work(x) => x,
State::Exit => panic!("state only set to exit on drop; queue live now; qed"),
}
}
/// Optimise memory footprint of the heap fields, and adjust the number of threads
/// to better suit the workload.
pub fn collect_garbage(&self) {
@ -598,13 +624,15 @@ impl<K: Kind> VerificationQueue<K> {
self.processing.write().shrink_to_fit();
if !self.scale_verifiers { return }
if self.ticks_since_adjustment.fetch_add(1, AtomicOrdering::SeqCst) + 1 >= READJUSTMENT_PERIOD {
self.ticks_since_adjustment.store(0, AtomicOrdering::SeqCst);
} else {
return;
}
let current = self.verifiers.lock().1;
let current = self.num_verifiers();
let diff = (v_len - u_len).abs();
let total = v_len + u_len;
@ -626,27 +654,14 @@ impl<K: Kind> VerificationQueue<K> {
// possible, never going over the amount of initially allocated threads
// or below 1.
fn scale_verifiers(&self, target: usize) {
let mut verifiers = self.verifiers.lock();
let &mut (ref mut verifiers, ref mut verifier_count) = &mut *verifiers;
let target = min(verifiers.len(), target);
let current = self.num_verifiers();
let target = min(self.verifier_handles.len(), target);
let target = max(1, target);
debug!(target: "verification", "Scaling from {} to {} verifiers", verifier_count, target);
debug!(target: "verification", "Scaling from {} to {} verifiers", current, target);
// scaling up
for i in *verifier_count..target {
debug!(target: "verification", "Waking up verifier {}", i);
verifiers[i].wake_up();
}
// scaling down.
for i in target..*verifier_count {
debug!(target: "verification", "Putting verifier {} to sleep", i);
verifiers[i].sleep();
}
*verifier_count = target;
*self.state.0.lock() = State::Work(target);
self.state.1.notify_all();
}
}
@ -660,22 +675,18 @@ impl<K: Kind> Drop for VerificationQueue<K> {
fn drop(&mut self) {
trace!(target: "shutdown", "[VerificationQueue] Closing...");
self.clear();
self.deleting.store(true, AtomicOrdering::Release);
self.deleting.store(true, AtomicOrdering::SeqCst);
let mut verifiers = self.verifiers.get_mut();
let mut verifiers = &mut verifiers.0;
// first pass to signal conclusion. must be done before
// notify or deadlock possible.
for handle in verifiers.iter() {
handle.conclude();
}
// set exit state; should be done before `more_to_verify` notification.
*self.state.0.lock() = State::Exit;
self.state.1.notify_all();
// wake up all threads waiting for more work.
self.more_to_verify.notify_all();
// second pass to join.
for handle in verifiers.drain(..) {
handle.join();
// wait for all verifier threads to join.
for thread in self.verifier_handles.drain(..) {
thread.join().expect("Propagating verifier thread panic on shutdown");
}
trace!(target: "shutdown", "[VerificationQueue] Closed.");
@ -687,16 +698,21 @@ mod tests {
use util::*;
use io::*;
use spec::*;
use super::{BlockQueue, Config};
use super::{BlockQueue, Config, State};
use super::kind::blocks::Unverified;
use tests::helpers::*;
use error::*;
use views::*;
fn get_test_queue() -> BlockQueue {
// create a test block queue.
// auto_scaling enables verifier adjustment.
fn get_test_queue(auto_scale: bool) -> BlockQueue {
let spec = get_test_spec();
let engine = spec.engine;
BlockQueue::new(Config::default(), engine, IoChannel::disconnected(), true)
let mut config = Config::default();
config.verifier_settings.scale_verifiers = auto_scale;
BlockQueue::new(config, engine, IoChannel::disconnected(), true)
}
#[test]
@ -709,7 +725,7 @@ mod tests {
#[test]
fn can_import_blocks() {
let queue = get_test_queue();
let queue = get_test_queue(false);
if let Err(e) = queue.import(Unverified::new(get_good_dummy_block())) {
panic!("error importing block that is valid by definition({:?})", e);
}
@ -717,7 +733,7 @@ mod tests {
#[test]
fn returns_error_for_duplicates() {
let queue = get_test_queue();
let queue = get_test_queue(false);
if let Err(e) = queue.import(Unverified::new(get_good_dummy_block())) {
panic!("error importing block that is valid by definition({:?})", e);
}
@ -736,7 +752,7 @@ mod tests {
#[test]
fn returns_ok_for_drained_duplicates() {
let queue = get_test_queue();
let queue = get_test_queue(false);
let block = get_good_dummy_block();
let hash = BlockView::new(&block).header().hash().clone();
if let Err(e) = queue.import(Unverified::new(block)) {
@ -753,7 +769,7 @@ mod tests {
#[test]
fn returns_empty_once_finished() {
let queue = get_test_queue();
let queue = get_test_queue(false);
queue.import(Unverified::new(get_good_dummy_block()))
.expect("error importing block that is valid by definition");
queue.flush();
@ -781,30 +797,23 @@ mod tests {
fn scaling_limits() {
use super::MAX_VERIFIERS;
let queue = get_test_queue();
let queue = get_test_queue(true);
queue.scale_verifiers(MAX_VERIFIERS + 1);
assert!(queue.verifiers.lock().1 < MAX_VERIFIERS + 1);
assert!(queue.num_verifiers() < MAX_VERIFIERS + 1);
queue.scale_verifiers(0);
assert!(queue.verifiers.lock().1 == 1);
assert!(queue.num_verifiers() == 1);
}
#[test]
fn readjust_verifiers() {
let queue = get_test_queue();
let queue = get_test_queue(true);
// put all the verifiers to sleep to ensure
// the test isn't timing sensitive.
let num_verifiers = {
let verifiers = queue.verifiers.lock();
for i in 0..verifiers.1 {
verifiers.0[i].sleep();
}
verifiers.1
};
*queue.state.0.lock() = State::Work(0);
for block in get_good_dummy_block_seq(5000) {
queue.import(Unverified::new(block)).expect("Block good by definition; qed");
@ -812,20 +821,12 @@ mod tests {
// almost all unverified == bump verifier count.
queue.collect_garbage();
assert_eq!(queue.verifiers.lock().1, num_verifiers + 1);
// wake them up again and verify everything.
{
let verifiers = queue.verifiers.lock();
for i in 0..verifiers.1 {
verifiers.0[i].wake_up();
}
}
assert_eq!(queue.num_verifiers(), 1);
queue.flush();
// nothing to verify == use minimum number of verifiers.
queue.collect_garbage();
assert_eq!(queue.verifiers.lock().1, 1);
assert_eq!(queue.num_verifiers(), 1);
}
}

View File

@ -1,6 +1,6 @@
{
"name": "parity.js",
"version": "0.2.91",
"version": "0.2.97",
"main": "release/index.js",
"jsnext:main": "src/index.js",
"author": "Parity Team <admin@parity.io>",

View File

@ -189,15 +189,21 @@ export default class Contract {
});
}
_encodeOptions (func, options, values) {
getCallData = (func, options, values) => {
let data = options.data;
const tokens = func ? this._abi.encodeTokens(func.inputParamTypes(), values) : null;
const call = tokens ? func.encodeCall(tokens) : null;
if (options.data && options.data.substr(0, 2) === '0x') {
options.data = options.data.substr(2);
if (data && data.substr(0, 2) === '0x') {
data = data.substr(2);
}
options.data = `0x${options.data || ''}${call || ''}`;
return `0x${data || ''}${call || ''}`;
}
_encodeOptions (func, options, values) {
options.data = this.getCallData(func, options, values);
return options;
}
@ -209,8 +215,10 @@ export default class Contract {
_bindFunction = (func) => {
func.call = (options, values = []) => {
const callParams = this._encodeOptions(func, this._addOptionsTo(options), values);
return this._api.eth
.call(this._encodeOptions(func, this._addOptionsTo(options), values))
.call(callParams)
.then((encoded) => func.decodeOutput(encoded))
.then((tokens) => tokens.map((token) => token.value))
.then((returns) => returns.length === 1 ? returns[0] : returns);

View File

@ -128,6 +128,11 @@ export default class Parity {
.execute('parity_killAccount', inAddress(account), password);
}
removeAddress (address) {
return this._transport
.execute('parity_removeAddress', inAddress(address));
}
listGethAccounts () {
return this._transport
.execute('parity_listGethAccounts')

View File

@ -0,0 +1,21 @@
// 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 wallet from './wallet';
export {
wallet
};

File diff suppressed because one or more lines are too long

View File

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

View File

@ -21,6 +21,9 @@ const muiTheme = getMuiTheme(lightBaseTheme);
import CircularProgress from 'material-ui/CircularProgress';
import { Card, CardText } from 'material-ui/Card';
import { nullableProptype } from '~/util/proptypes';
import styles from './application.css';
import Accounts from '../Accounts';
import Events from '../Events';
@ -28,8 +31,6 @@ import Lookup from '../Lookup';
import Names from '../Names';
import Records from '../Records';
const nullable = (type) => React.PropTypes.oneOfType([ React.PropTypes.oneOf([ null ]), type ]);
export default class Application extends Component {
static childContextTypes = {
muiTheme: PropTypes.object.isRequired,
@ -44,8 +45,8 @@ export default class Application extends Component {
actions: PropTypes.object.isRequired,
accounts: PropTypes.object.isRequired,
contacts: PropTypes.object.isRequired,
contract: nullable(PropTypes.object.isRequired),
fee: nullable(PropTypes.object.isRequired),
contract: nullableProptype(PropTypes.object.isRequired),
fee: nullableProptype(PropTypes.object.isRequired),
lookup: PropTypes.object.isRequired,
events: PropTypes.object.isRequired,
names: PropTypes.object.isRequired,

View File

@ -18,19 +18,19 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { nullableProptype } from '~/util/proptypes';
import Application from './Application';
import * as actions from './actions';
const nullable = (type) => React.PropTypes.oneOfType([ React.PropTypes.oneOf([ null ]), type ]);
class Container extends Component {
static propTypes = {
actions: PropTypes.object.isRequired,
accounts: PropTypes.object.isRequired,
contacts: PropTypes.object.isRequired,
contract: nullable(PropTypes.object.isRequired),
owner: nullable(PropTypes.string.isRequired),
fee: nullable(PropTypes.object.isRequired),
contract: nullableProptype(PropTypes.object.isRequired),
owner: nullableProptype(PropTypes.string.isRequired),
fee: nullableProptype(PropTypes.object.isRequired),
lookup: PropTypes.object.isRequired,
events: PropTypes.object.isRequired
};

View File

@ -19,21 +19,22 @@ import { Card, CardHeader, CardText } from 'material-ui/Card';
import TextField from 'material-ui/TextField';
import RaisedButton from 'material-ui/RaisedButton';
import SearchIcon from 'material-ui/svg-icons/action/search';
import { nullableProptype } from '~/util/proptypes';
import renderAddress from '../ui/address.js';
import renderImage from '../ui/image.js';
import recordTypeSelect from '../ui/record-type-select.js';
import styles from './lookup.css';
const nullable = (type) => React.PropTypes.oneOfType([ React.PropTypes.oneOf([ null ]), type ]);
export default class Lookup extends Component {
static propTypes = {
actions: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
result: nullable(PropTypes.string.isRequired),
result: nullableProptype(PropTypes.string.isRequired),
accounts: PropTypes.object.isRequired,
contacts: PropTypes.object.isRequired
}

View File

@ -1,3 +1,19 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { Card, CardHeader, CardText } from 'material-ui/Card';
import TextField from 'material-ui/TextField';

View File

@ -256,6 +256,20 @@ export default {
}
},
removeAddress: {
desc: 'Removes an address from the addressbook',
params: [
{
type: Address,
desc: 'The address to remove'
}
],
returns: {
type: Boolean,
desc: 'true on success'
}
},
listGethAccounts: {
desc: 'Returns a list of the accounts available from Geth',
params: [],

View File

@ -17,7 +17,7 @@
import React, { Component, PropTypes } from 'react';
import { Redirect, Router, Route } from 'react-router';
import { Accounts, Account, Addresses, Address, Application, Contract, Contracts, WriteContract, 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';
import styles from './reset.css';
@ -37,6 +37,7 @@ export default class MainApplication extends Component {
<Route path='/' component={ Application }>
<Route path='accounts' component={ Accounts } />
<Route path='account/:address' component={ Account } />
<Route path='wallet/:address' component={ Wallet } />
<Route path='addresses' component={ Addresses } />
<Route path='address/:address' component={ Address } />
<Route path='apps' component={ Dapps } />

View File

@ -192,8 +192,6 @@ export default class CreateAccount extends Component {
};
});
console.log(accounts);
this.setState({
selectedAddress: addresses[0],
accounts: accounts
@ -201,8 +199,7 @@ export default class CreateAccount extends Component {
});
})
.catch((error) => {
console.log('createIdentities', error);
console.error('createIdentities', error);
setTimeout(this.createIdentities, 1000);
this.newError(error);
});

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './walletDetails';

View File

@ -0,0 +1,162 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { Form, TypedInput, Input, AddressSelect, InputAddress } from '~/ui';
import { parseAbiType } from '~/util/abi';
import styles from '../createWallet.css';
export default class WalletDetails extends Component {
static propTypes = {
accounts: PropTypes.object.isRequired,
wallet: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
walletType: PropTypes.string.isRequired
};
render () {
const { walletType } = this.props;
if (walletType === 'WATCH') {
return this.renderWatchDetails();
}
return this.renderMultisigDetails();
}
renderWatchDetails () {
const { wallet, errors } = this.props;
return (
<Form>
<InputAddress
label='wallet address'
hint='the wallet contract address'
value={ wallet.address }
error={ errors.address }
onChange={ this.onAddressChange }
/>
<Input
label='wallet name'
hint='the local name for this wallet'
value={ wallet.name }
error={ errors.name }
onChange={ this.onNameChange }
/>
<Input
label='wallet description (optional)'
hint='the local description for this wallet'
value={ wallet.description }
onChange={ this.onDescriptionChange }
/>
</Form>
);
}
renderMultisigDetails () {
const { accounts, wallet, errors } = this.props;
return (
<Form>
<AddressSelect
label='from account (contract owner)'
hint='the owner account for this contract'
value={ wallet.account }
error={ errors.account }
onChange={ this.onAccoutChange }
accounts={ accounts }
/>
<Input
label='wallet name'
hint='the local name for this wallet'
value={ wallet.name }
error={ errors.name }
onChange={ this.onNameChange }
/>
<Input
label='wallet description (optional)'
hint='the local description for this wallet'
value={ wallet.description }
onChange={ this.onDescriptionChange }
/>
<TypedInput
label='other wallet owners'
value={ wallet.owners.slice() }
onChange={ this.onOwnersChange }
accounts={ accounts }
param={ parseAbiType('address[]') }
/>
<div className={ styles.splitInput }>
<TypedInput
label='required owners'
hint='number of required owners to accept a transaction'
value={ wallet.required }
error={ errors.required }
onChange={ this.onRequiredChange }
param={ parseAbiType('uint') }
min={ 1 }
/>
<TypedInput
label='wallet day limit'
hint='number of days to wait for other owners confirmation'
value={ wallet.daylimit }
error={ errors.daylimit }
onChange={ this.onDaylimitChange }
param={ parseAbiType('uint') }
/>
</div>
</Form>
);
}
onAddressChange = (_, address) => {
this.props.onChange({ address });
}
onAccoutChange = (_, account) => {
this.props.onChange({ account });
}
onNameChange = (_, name) => {
this.props.onChange({ name });
}
onDescriptionChange = (_, description) => {
this.props.onChange({ description });
}
onOwnersChange = (owners) => {
this.props.onChange({ owners });
}
onRequiredChange = (required) => {
this.props.onChange({ required });
}
onDaylimitChange = (daylimit) => {
this.props.onChange({ daylimit });
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './walletInfo';

View File

@ -0,0 +1,91 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { CompletedStep, IdentityIcon, CopyToClipboard } from '~/ui';
import styles from '../createWallet.css';
export default class WalletInfo extends Component {
static propTypes = {
accounts: PropTypes.object.isRequired,
account: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
address: PropTypes.string.isRequired,
owners: PropTypes.array.isRequired,
required: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired,
daylimit: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired,
deployed: PropTypes.bool
};
render () {
const { address, required, daylimit, name, deployed } = this.props;
return (
<CompletedStep>
<div>
<code>{ name }</code>
<span> has been </span>
<span> { deployed ? 'deployed' : 'added' } at </span>
</div>
<div>
<CopyToClipboard data={ address } label='copy address to clipboard' />
<IdentityIcon address={ address } inline center className={ styles.identityicon } />
<div className={ styles.address }>{ address }</div>
</div>
<div>with the following owners</div>
<div>
{ this.renderOwners() }
</div>
<p>
<code>{ required }</code> owners are required to confirm a transaction.
</p>
<p>
The daily limit is set to <code>{ daylimit }</code>.
</p>
</CompletedStep>
);
}
renderOwners () {
const { account, owners, deployed } = this.props;
return [].concat(deployed ? account : null, owners).filter((a) => a).map((address, id) => (
<div key={ id } className={ styles.owner }>
<IdentityIcon address={ address } inline center className={ styles.identityicon } />
<div className={ styles.address }>{ this.addressToString(address) }</div>
</div>
));
}
addressToString (address) {
const { accounts } = this.props;
if (accounts[address]) {
return accounts[address].name || address;
}
return address;
}
}

View File

@ -1,19 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
//
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.extern crate libc;
extern crate libc;
mod raise_fd_limit;
pub use raise_fd_limit::raise_fd_limit;
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './walletType.js';

View File

@ -0,0 +1,58 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { RadioButtons } from '~/ui';
// import styles from '../createWallet.css';
export default class WalletType extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
type: PropTypes.string.isRequired
};
render () {
const { type } = this.props;
return (
<RadioButtons
name='contractType'
value={ type }
values={ this.getTypes() }
onChange={ this.onTypeChange }
/>
);
}
getTypes () {
return [
{
label: 'Multi-Sig wallet', key: 'MULTISIG',
description: 'A standard multi-signature Wallet'
},
{
label: 'Watch a wallet', key: 'WATCH',
description: 'Add an existing wallet to your accounts'
}
];
}
onTypeChange = (type) => {
this.props.onChange(type.key);
}
}

View File

@ -0,0 +1,58 @@
/* 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/>.
*/
.address {
vertical-align: top;
display: inline-block;
}
.identityicon {
margin: -8px 0.5em;
}
.owner {
height: 40px;
color: lightgrey;
display: flex;
align-items: center;
justify-content: center;
.identityicon {
width: 24px;
height: 24px;
}
}
.splitInput {
display: flex;
flex-direction: row;
> * {
flex: 1;
margin: 0 0.25em;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
}

View File

@ -0,0 +1,214 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { observer } from 'mobx-react';
import ActionDone from 'material-ui/svg-icons/action/done';
import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import { Button, Modal, TxHash, BusyStep } from '~/ui';
import WalletType from './WalletType';
import WalletDetails from './WalletDetails';
import WalletInfo from './WalletInfo';
import CreateWalletStore from './createWalletStore';
// import styles from './createWallet.css';
@observer
export default class CreateWallet extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
accounts: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired
};
store = new CreateWalletStore(this.context.api, this.props.accounts);
render () {
const { stage, steps, waiting, rejected } = this.store;
if (rejected) {
return (
<Modal
visible
title='rejected'
actions={ this.renderDialogActions() }
>
<BusyStep
title='The deployment has been rejected'
state='The wallet will not be created. You can safely close this window.'
/>
</Modal>
);
}
return (
<Modal
visible
actions={ this.renderDialogActions() }
current={ stage }
steps={ steps.map((s) => s.title) }
waiting={ waiting }
>
{ this.renderPage() }
</Modal>
);
}
renderPage () {
const { step } = this.store;
const { accounts } = this.props;
switch (step) {
case 'DEPLOYMENT':
return (
<BusyStep
title='The deployment is currently in progress'
state={ this.store.deployState }
>
{ this.store.txhash ? (<TxHash hash={ this.store.txhash } />) : null }
</BusyStep>
);
case 'INFO':
return (
<WalletInfo
accounts={ accounts }
account={ this.store.wallet.account }
address={ this.store.wallet.address }
owners={ this.store.wallet.owners.slice() }
required={ this.store.wallet.required }
daylimit={ this.store.wallet.daylimit }
name={ this.store.wallet.name }
deployed={ this.store.deployed }
/>
);
case 'DETAILS':
return (
<WalletDetails
accounts={ accounts }
wallet={ this.store.wallet }
errors={ this.store.errors }
walletType={ this.store.walletType }
onChange={ this.store.onChange }
/>
);
default:
case 'TYPE':
return (
<WalletType
onChange={ this.store.onTypeChange }
type={ this.store.walletType }
/>
);
}
}
renderDialogActions () {
const { step, hasErrors, rejected, onCreate, onNext, onAdd } = this.store;
const cancelBtn = (
<Button
icon={ <ContentClear /> }
label='Cancel'
onClick={ this.onClose }
/>
);
const closeBtn = (
<Button
icon={ <ContentClear /> }
label='Close'
onClick={ this.onClose }
/>
);
const doneBtn = (
<Button
icon={ <ActionDone /> }
label='Done'
onClick={ this.onClose }
/>
);
const sendingBtn = (
<Button
icon={ <ActionDone /> }
label='Sending...'
disabled
/>
);
const nextBtn = (
<Button
icon={ <NavigationArrowForward /> }
label='Next'
onClick={ onNext }
/>
);
if (rejected) {
return [ closeBtn ];
}
switch (step) {
case 'DEPLOYMENT':
return [ closeBtn, sendingBtn ];
case 'INFO':
return [ doneBtn ];
case 'DETAILS':
if (this.store.walletType === 'WATCH') {
return [ cancelBtn, (
<Button
icon={ <NavigationArrowForward /> }
label='Add'
disabled={ hasErrors }
onClick={ onAdd }
/>
) ];
}
return [ cancelBtn, (
<Button
icon={ <NavigationArrowForward /> }
label='Create'
disabled={ hasErrors }
onClick={ onCreate }
/>
) ];
default:
case 'TYPE':
return [ cancelBtn, nextBtn ];
}
}
onClose = () => {
this.props.onClose();
}
}

View File

@ -0,0 +1,272 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observable, computed, action, transaction } from 'mobx';
import { validateUint, validateAddress, validateName } from '../../util/validation';
import { ERROR_CODES } from '~/api/transport/error';
import Contract from '~/api/contract';
import { wallet as walletAbi } from '~/contracts/abi';
import { wallet as walletCode } from '~/contracts/code';
import WalletsUtils from '~/util/wallets';
const STEPS = {
TYPE: { title: 'wallet type' },
DETAILS: { title: 'wallet details' },
DEPLOYMENT: { title: 'wallet deployment', waiting: true },
INFO: { title: 'wallet informaton' }
};
export default class CreateWalletStore {
@observable step = null;
@observable rejected = false;
@observable deployState = null;
@observable deployError = null;
@observable deployed = false;
@observable txhash = null;
@observable wallet = {
account: '',
address: '',
owners: [],
required: 1,
daylimit: 0,
name: '',
description: ''
};
@observable walletType = 'MULTISIG';
@observable errors = {
account: null,
address: null,
owners: null,
required: null,
daylimit: null,
name: null
};
@computed get stage () {
return this.stepsKeys.findIndex((k) => k === this.step);
}
@computed get hasErrors () {
return !!Object.keys(this.errors)
.filter((errorKey) => {
if (this.walletType === 'WATCH') {
return ['address', 'name'].includes(errorKey);
}
return errorKey !== 'address';
})
.find((key) => !!this.errors[key]);
}
@computed get stepsKeys () {
return this.steps.map((s) => s.key);
}
@computed get steps () {
return Object
.keys(STEPS)
.map((key) => {
return {
...STEPS[key],
key
};
})
.filter((step) => {
return (this.walletType !== 'WATCH' || step.key !== 'DEPLOYMENT');
});
}
@computed get waiting () {
this.steps
.map((s, idx) => ({ idx, waiting: s.waiting }))
.filter((s) => s.waiting)
.map((s) => s.idx);
}
constructor (api, accounts) {
this.api = api;
this.step = this.stepsKeys[0];
this.wallet.account = Object.values(accounts)[0].address;
this.validateWallet(this.wallet);
}
@action onTypeChange = (type) => {
this.walletType = type;
this.validateWallet(this.wallet);
}
@action onNext = () => {
const stepIndex = this.stepsKeys.findIndex((k) => k === this.step) + 1;
this.step = this.stepsKeys[stepIndex];
}
@action onChange = (_wallet) => {
const newWallet = Object.assign({}, this.wallet, _wallet);
this.validateWallet(newWallet);
}
@action onAdd = () => {
if (this.hasErrors) {
return;
}
const walletContract = new Contract(this.api, walletAbi).at(this.wallet.address);
return Promise
.all([
WalletsUtils.fetchRequire(walletContract),
WalletsUtils.fetchOwners(walletContract),
WalletsUtils.fetchDailylimit(walletContract)
])
.then(([ require, owners, dailylimit ]) => {
transaction(() => {
this.wallet.owners = owners;
this.wallet.required = require.toNumber();
this.wallet.dailylimit = dailylimit.limit;
});
return this.addWallet(this.wallet);
});
}
@action onCreate = () => {
if (this.hasErrors) {
return;
}
this.step = 'DEPLOYMENT';
const { account, owners, required, daylimit } = this.wallet;
const options = {
data: walletCode,
from: account
};
this.api
.newContract(walletAbi)
.deploy(options, [ owners, required, daylimit ], this.onDeploymentState)
.then((address) => {
this.deployed = true;
this.wallet.address = address;
return this.addWallet(this.wallet);
})
.catch((error) => {
if (error.code === ERROR_CODES.REQUEST_REJECTED) {
this.rejected = true;
return;
}
console.error('error deploying contract', error);
this.deployError = error;
});
}
@action addWallet = (wallet) => {
const { address, name, description } = wallet;
return Promise
.all([
this.api.parity.setAccountName(address, name),
this.api.parity.setAccountMeta(address, {
abi: walletAbi,
wallet: true,
timestamp: Date.now(),
deleted: false,
description,
name,
tags: ['wallet']
})
])
.then(() => {
this.step = 'INFO';
});
}
onDeploymentState = (error, data) => {
if (error) {
return console.error('createWallet::onDeploymentState', error);
}
switch (data.state) {
case 'estimateGas':
case 'postTransaction':
this.deployState = 'Preparing transaction for network transmission';
return;
case 'checkRequest':
this.deployState = 'Waiting for confirmation of the transaction in the Parity Secure Signer';
return;
case 'getTransactionReceipt':
this.deployState = 'Waiting for the contract deployment transaction receipt';
this.txhash = data.txhash;
return;
case 'hasReceipt':
case 'getCode':
this.deployState = 'Validating the deployed contract code';
return;
case 'completed':
this.deployState = 'The contract deployment has been completed';
return;
default:
console.error('createWallet::onDeploymentState', 'unknow contract deployment state', data);
return;
}
}
@action validateWallet = (_wallet) => {
const addressValidation = validateAddress(_wallet.address);
const accountValidation = validateAddress(_wallet.account);
const requiredValidation = validateUint(_wallet.required);
const daylimitValidation = validateUint(_wallet.daylimit);
const nameValidation = validateName(_wallet.name);
const errors = {
address: addressValidation.addressError,
account: accountValidation.addressError,
required: requiredValidation.valueError,
daylimit: daylimitValidation.valueError,
name: nameValidation.nameError
};
const wallet = {
..._wallet,
address: addressValidation.address,
account: accountValidation.address,
required: requiredValidation.value,
daylimit: daylimitValidation.value,
name: nameValidation.name
};
transaction(() => {
this.wallet = wallet;
this.errors = errors;
});
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './createWallet';

View File

@ -19,7 +19,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { ConfirmDialog, IdentityIcon, IdentityName, Input } from '~/ui';
import { newError } from '../../redux/actions';
import { newError } from '~/redux/actions';
import styles from './deleteAccount.css';

View File

@ -15,7 +15,6 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import nullable from '../../../util/nullable-proptype';
import BigNumber from 'bignumber.js';
import { Checkbox } from 'material-ui';
import InfoIcon from 'material-ui/svg-icons/action/info-outline';
@ -24,6 +23,7 @@ import ErrorIcon from 'material-ui/svg-icons/navigation/close';
import { fromWei } from '~/api/util/wei';
import { Form, Input } from '~/ui';
import { nullableProptype } from '~/util/proptypes';
import { termsOfService } from '../../../3rdparty/sms-verification';
import styles from './gatherData.css';
@ -32,8 +32,8 @@ export default class GatherData extends Component {
static propTypes = {
fee: React.PropTypes.instanceOf(BigNumber),
isNumberValid: PropTypes.bool.isRequired,
isVerified: nullable(PropTypes.bool.isRequired),
hasRequested: nullable(PropTypes.bool.isRequired),
isVerified: nullableProptype(PropTypes.bool.isRequired),
hasRequested: nullableProptype(PropTypes.bool.isRequired),
setNumber: PropTypes.func.isRequired,
setConsentGiven: PropTypes.func.isRequired
}

View File

@ -15,8 +15,8 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import nullable from '../../../util/nullable-proptype';
import { nullableProptype } from '~/util/proptypes';
import TxHash from '~/ui/TxHash';
import {
POSTING_CONFIRMATION, POSTED_CONFIRMATION
@ -27,7 +27,7 @@ import styles from './sendConfirmation.css';
export default class SendConfirmation extends Component {
static propTypes = {
step: PropTypes.any.isRequired,
tx: nullable(PropTypes.any.isRequired)
tx: nullableProptype(PropTypes.any.isRequired)
}
render () {

View File

@ -15,8 +15,8 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import nullable from '../../../util/nullable-proptype';
import { nullableProptype } from '~/util/proptypes';
import TxHash from '~/ui/TxHash';
import {
POSTING_REQUEST, POSTED_REQUEST, REQUESTING_SMS
@ -27,7 +27,7 @@ import styles from './sendRequest.css';
export default class SendRequest extends Component {
static propTypes = {
step: PropTypes.any.isRequired,
tx: nullable(PropTypes.any.isRequired)
tx: nullableProptype(PropTypes.any.isRequired)
}
render () {

View File

@ -17,10 +17,10 @@
import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import { Checkbox, MenuItem } from 'material-ui';
import { isEqual } from 'lodash';
import Form, { Input, InputAddressSelect, Select } from '~/ui/Form';
import Form, { Input, InputAddressSelect, AddressSelect, Select } from '~/ui/Form';
import { nullableProptype } from '~/util/proptypes';
import imageUnknown from '../../../../assets/images/contracts/unknown-64x64.png';
import styles from '../transfer.css';
@ -132,6 +132,8 @@ export default class Details extends Component {
all: PropTypes.bool,
extras: PropTypes.bool,
images: PropTypes.object.isRequired,
sender: PropTypes.string,
senderError: PropTypes.string,
recipient: PropTypes.string,
recipientError: PropTypes.string,
tag: PropTypes.string,
@ -139,8 +141,15 @@ export default class Details extends Component {
totalError: PropTypes.string,
value: PropTypes.string,
valueError: PropTypes.string,
onChange: PropTypes.func.isRequired
}
onChange: PropTypes.func.isRequired,
wallet: PropTypes.object,
senders: nullableProptype(PropTypes.object)
};
static defaultProps = {
wallet: null,
senders: null
};
render () {
const { all, extras, tag, total, totalError, value, valueError } = this.props;
@ -149,6 +158,7 @@ export default class Details extends Component {
return (
<Form>
{ this.renderTokenSelect() }
{ this.renderFromAddress() }
{ this.renderToAddress() }
<div className={ styles.columns }>
<div>
@ -179,6 +189,7 @@ export default class Details extends Component {
</div>
</Input>
</div>
<div>
<Checkbox
checked={ extras }
@ -191,6 +202,27 @@ export default class Details extends Component {
);
}
renderFromAddress () {
const { sender, senderError, senders } = this.props;
if (!senders) {
return null;
}
return (
<div className={ styles.address }>
<AddressSelect
accounts={ senders }
error={ senderError }
label='sender address'
hint='the sender address'
value={ sender }
onChange={ this.onEditSender }
/>
</div>
);
}
renderToAddress () {
const { recipient, recipientError } = this.props;
@ -223,6 +255,10 @@ export default class Details extends Component {
this.props.onChange('tag', tag);
}
onEditSender = (event, sender) => {
this.props.onChange('sender', sender);
}
onEditRecipient = (event, recipient) => {
this.props.onChange('recipient', recipient);
}

View File

@ -16,7 +16,11 @@
import { observable, computed, action, transaction } from 'mobx';
import BigNumber from 'bignumber.js';
import { uniq } from 'lodash';
import { wallet as walletAbi } from '~/contracts/abi';
import { bytesToHex } from '~/api/util/format';
import Contract from '~/api/contract';
import ERRORS from './errors';
import { ERROR_CODES } from '~/api/transport/error';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '../../util/constants';
@ -33,28 +37,37 @@ const STAGES_EXTRA = [TITLES.transfer, TITLES.extras, TITLES.sending, TITLES.com
export default class TransferStore {
@observable stage = 0;
@observable data = '';
@observable dataError = null;
@observable extras = false;
@observable gas = DEFAULT_GAS;
@observable gasEst = '0';
@observable gasError = null;
@observable gasLimitError = null;
@observable gasPrice = DEFAULT_GASPRICE;
@observable gasPriceError = null;
@observable recipient = '';
@observable recipientError = ERRORS.requireRecipient;
@observable valueAll = false;
@observable sending = false;
@observable tag = 'ETH';
@observable total = '0.0';
@observable totalError = null;
@observable value = '0.0';
@observable valueAll = false;
@observable valueError = null;
@observable isEth = true;
@observable busyState = null;
@observable rejected = false;
@observable data = '';
@observable dataError = null;
@observable gas = DEFAULT_GAS;
@observable gasError = null;
@observable gasEst = '0';
@observable gasLimitError = null;
@observable gasPrice = DEFAULT_GASPRICE;
@observable gasPriceError = null;
@observable recipient = '';
@observable recipientError = ERRORS.requireRecipient;
@observable sender = '';
@observable senderError = null;
@observable total = '0.0';
@observable totalError = null;
@observable value = '0.0';
@observable valueError = null;
gasPriceHistogram = {};
account = null;
@ -62,6 +75,12 @@ export default class TransferStore {
gasLimit = null;
onClose = null;
senders = null;
sendersBalances = null;
isWallet = false;
wallet = null;
@computed get steps () {
const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC);
@ -73,7 +92,7 @@ export default class TransferStore {
}
@computed get isValid () {
const detailsValid = !this.recipientError && !this.valueError && !this.totalError;
const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError;
const extrasValid = !this.gasError && !this.gasPriceError && !this.totalError;
const verifyValid = !this.passwordError;
@ -89,15 +108,32 @@ export default class TransferStore {
}
}
get token () {
return this.balance.tokens.find((balance) => balance.token.tag === this.tag).token;
}
constructor (api, props) {
this.api = api;
const { account, balance, gasLimit, onClose } = props;
const { account, balance, gasLimit, senders, onClose, newError, sendersBalances } = props;
this.account = account;
this.balance = balance;
this.gasLimit = gasLimit;
this.onClose = onClose;
this.isWallet = account && account.wallet;
this.newError = newError;
if (this.isWallet) {
this.wallet = props.wallet;
this.walletContract = new Contract(this.api, walletAbi);
}
if (senders) {
this.senders = senders;
this.sendersBalances = sendersBalances;
this.senderError = ERRORS.requireSender;
}
}
@action onNext = () => {
@ -133,6 +169,9 @@ export default class TransferStore {
case 'recipient':
return this._onUpdateRecipient(value);
case 'sender':
return this._onUpdateSender(value);
case 'tag':
return this._onUpdateTag(value);
@ -165,9 +204,8 @@ export default class TransferStore {
this.onNext();
this.sending = true;
const promise = this.isEth ? this._sendEth() : this._sendToken();
promise
this
.send()
.then((requestId) => {
this.busyState = 'Waiting for authorization in the Parity Signer';
@ -190,6 +228,10 @@ export default class TransferStore {
this.txhash = txhash;
this.busyState = 'Your transaction has been posted to the network';
});
if (this.isWallet) {
return this._attachWalletOperation(txhash);
}
})
.catch((error) => {
this.sending = false;
@ -197,6 +239,34 @@ export default class TransferStore {
});
}
@action _attachWalletOperation = (txhash) => {
let ethSubscriptionId = null;
return this.api.subscribe('eth_blockNumber', () => {
this.api.eth
.getTransactionReceipt(txhash)
.then((tx) => {
if (!tx) {
return;
}
const logs = this.walletContract.parseEventLogs(tx.logs);
const operations = uniq(logs
.filter((log) => log && log.params && log.params.operation)
.map((log) => bytesToHex(log.params.operation.value)));
if (operations.length > 0) {
this.operation = operations[0];
}
this.api.unsubscribe(ethSubscriptionId);
ethSubscriptionId = null;
});
}).then((subId) => {
ethSubscriptionId = subId;
});
}
@action _onUpdateAll = (valueAll) => {
this.valueAll = valueAll;
this.recalculateGas();
@ -250,6 +320,23 @@ export default class TransferStore {
});
}
@action _onUpdateSender = (sender) => {
let senderError = null;
if (!sender || !sender.length) {
senderError = ERRORS.requireSender;
} else if (!this.api.util.isAddressValid(sender)) {
senderError = ERRORS.invalidAddress;
}
transaction(() => {
this.sender = sender;
this.senderError = senderError;
this.recalculateGas();
});
}
@action _onUpdateTag = (tag) => {
transaction(() => {
this.tag = tag;
@ -280,9 +367,8 @@ export default class TransferStore {
return this.recalculate();
}
const promise = this.isEth ? this._estimateGasEth() : this._estimateGasToken();
promise
this
.estimateGas()
.then((gasEst) => {
let gas = gasEst;
let gasLimitError = null;
@ -312,19 +398,29 @@ export default class TransferStore {
}
@action recalculate = () => {
const { account, balance } = this;
const { account } = this;
if (!account || !balance) {
if (!account || !this.balance) {
return;
}
const balance = this.senders
? this.sendersBalances[this.sender]
: this.balance;
if (!balance) {
return;
}
const { gas, gasPrice, tag, valueAll, isEth } = this;
const gasTotal = new BigNumber(gasPrice || 0).mul(new BigNumber(gas || 0));
const balance_ = balance.tokens.find((b) => tag === b.token.tag);
const availableEth = new BigNumber(balance.tokens[0].value);
const available = new BigNumber(balance_.value);
const format = new BigNumber(balance_.token.format || 1);
const senderBalance = this.balance.tokens.find((b) => tag === b.token.tag);
const available = new BigNumber(senderBalance.value);
const format = new BigNumber(senderBalance.token.format || 1);
let { value, valueError } = this;
let totalEth = gasTotal;
@ -361,74 +457,99 @@ export default class TransferStore {
});
}
_sendEth () {
const { account, data, gas, gasPrice, recipient, value } = this;
send () {
const { options, values } = this._getTransferParams();
return this._getTransferMethod().postTransaction(options, values);
}
_estimateGas (forceToken = false) {
const { options, values } = this._getTransferParams(true, forceToken);
return this._getTransferMethod(true, forceToken).estimateGas(options, values);
}
estimateGas () {
if (this.isEth || !this.isWallet) {
return this._estimateGas();
}
return Promise
.all([
this._estimateGas(true),
this._estimateGas()
])
.then((results) => results[0].plus(results[1]));
}
_getTransferMethod (gas = false, forceToken = false) {
const { isEth, isWallet } = this;
if (isEth && !isWallet && !forceToken) {
return gas ? this.api.eth : this.api.parity;
}
if (isWallet && !forceToken) {
return this.wallet.instance.execute;
}
return this.token.contract.instance.transfer;
}
_getData (gas = false) {
const { isEth, isWallet } = this;
if (!isWallet || isEth) {
return this.data && this.data.length ? this.data : '';
}
const func = this._getTransferMethod(gas, true);
const { options, values } = this._getTransferParams(gas, true);
return this.token.contract.getCallData(func, options, values);
}
_getTransferParams (gas = false, forceToken = false) {
const { isEth, isWallet } = this;
const to = (isEth && !isWallet) ? this.recipient
: (this.isWallet ? this.wallet.address : this.token.address);
const options = {
from: account.address,
to: recipient,
gas,
gasPrice,
value: this.api.util.toWei(value || 0)
from: this.sender || this.account.address,
to
};
if (data && data.length) {
options.data = data;
if (!gas) {
options.gas = this.gas;
options.gasPrice = this.gasPrice;
} else {
options.gas = MAX_GAS_ESTIMATION;
}
return this.api.parity.postTransaction(options);
if (isEth && !isWallet && !forceToken) {
options.value = this.api.util.toWei(this.value || 0);
options.data = this._getData(gas);
return { options, values: [] };
}
_sendToken () {
const { account, balance } = this;
const { gas, gasPrice, recipient, value, tag } = this;
if (isWallet && !forceToken) {
const to = isEth ? this.recipient : this.token.contract.address;
const value = isEth ? this.api.util.toWei(this.value || 0) : new BigNumber(0);
const token = balance.tokens.find((balance) => balance.token.tag === tag).token;
const values = [
to, value,
this._getData(gas)
];
return token.contract.instance.transfer
.postTransaction({
from: account.address,
to: token.address,
gas,
gasPrice
}, [
recipient,
new BigNumber(value).mul(token.format).toFixed(0)
]);
return { options, values };
}
_estimateGasToken () {
const { account, balance } = this;
const { recipient, value, tag } = this;
const values = [
this.recipient,
new BigNumber(this.value || 0).mul(this.token.format).toFixed(0)
];
const token = balance.tokens.find((balance) => balance.token.tag === tag).token;
return token.contract.instance.transfer
.estimateGas({
gas: MAX_GAS_ESTIMATION,
from: account.address,
to: token.address
}, [
recipient,
new BigNumber(value || 0).mul(token.format).toFixed(0)
]);
}
_estimateGasEth () {
const { account, data, recipient, value } = this;
const options = {
gas: MAX_GAS_ESTIMATION,
from: account.address,
to: recipient,
value: this.api.util.toWei(value || 0)
};
if (data && data.length) {
options.data = data;
}
return this.api.eth.estimateGas(options);
return { options, values };
}
_validatePositiveNumber (num) {

View File

@ -18,6 +18,7 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { observer } from 'mobx-react';
import { pick } from 'lodash';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear';
@ -25,7 +26,8 @@ import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import { newError } from '~/ui/Errors/actions';
import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash } from '~/ui';
import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash, Input } from '~/ui';
import { nullableProptype } from '~/util/proptypes';
import Details from './Details';
import Extras from './Extras';
@ -44,9 +46,11 @@ class Transfer extends Component {
gasLimit: PropTypes.object.isRequired,
images: PropTypes.object.isRequired,
senders: nullableProptype(PropTypes.object),
sendersBalances: nullableProptype(PropTypes.object),
account: PropTypes.object,
balance: PropTypes.object,
balances: PropTypes.object,
wallet: PropTypes.object,
onClose: PropTypes.func
}
@ -130,14 +134,33 @@ class Transfer extends Component {
return (
<CompletedStep>
<TxHash hash={ txhash } />
{
this.store.operation
? (
<div>
<br />
<p>
This transaction needs confirmation from other owners.
<Input
style={ { width: '50%', margin: '0 auto' } }
value={ this.store.operation }
label='operation hash'
readOnly
allowCopy
/>
</p>
</div>
)
: null
}
</CompletedStep>
);
}
renderDetailsPage () {
const { account, balance, images } = this.props;
const { valueAll, extras, recipient, recipientError, tag } = this.store;
const { total, totalError, value, valueError } = this.store;
const { account, balance, images, senders } = this.props;
const { valueAll, extras, recipient, recipientError, sender, senderError } = this.store;
const { tag, total, totalError, value, valueError } = this.store;
return (
<Details
@ -146,14 +169,19 @@ class Transfer extends Component {
balance={ balance }
extras={ extras }
images={ images }
senders={ senders }
recipient={ recipient }
recipientError={ recipientError }
sender={ sender }
senderError={ senderError }
tag={ tag }
total={ total }
totalError={ totalError }
value={ value }
valueError={ valueError }
onChange={ this.store.onUpdateDetails } />
onChange={ this.store.onUpdateDetails }
wallet={ account.wallet && this.props.wallet }
/>
);
}
@ -249,9 +277,30 @@ class Transfer extends Component {
}
}
function mapStateToProps (state) {
function mapStateToProps (initState, initProps) {
const { address } = initProps.account;
const isWallet = initProps.account && initProps.account.wallet;
const wallet = isWallet
? initState.wallet.wallets[address]
: null;
const senders = isWallet
? Object
.values(initState.personal.accounts)
.filter((account) => wallet.owners.includes(account.address))
.reduce((accounts, account) => {
accounts[account.address] = account;
return accounts;
}, {})
: null;
return (state) => {
const { gasLimit } = state.nodeStatus;
return { gasLimit };
const sendersBalances = senders ? pick(state.balances.balances, Object.keys(senders)) : null;
return { gasLimit, wallet, senders, sendersBalances };
};
}
function mapDispatchToProps (dispatch) {

View File

@ -17,6 +17,7 @@
import AddAddress from './AddAddress';
import AddContract from './AddContract';
import CreateAccount from './CreateAccount';
import CreateWallet from './CreateWallet';
import DeleteAccount from './DeleteAccount';
import DeployContract from './DeployContract';
import EditMeta from './EditMeta';
@ -33,6 +34,7 @@ export {
AddAddress,
AddContract,
CreateAccount,
CreateWallet,
DeleteAccount,
DeployContract,
EditMeta,

View File

@ -113,7 +113,7 @@ export function fetchTokens (_tokenIds) {
export function fetchBalances (_addresses) {
return (dispatch, getState) => {
const { api, personal } = getState();
const { visibleAccounts } = personal;
const { visibleAccounts, accounts } = personal;
const addresses = uniq(_addresses || visibleAccounts || []);
@ -123,12 +123,14 @@ export function fetchBalances (_addresses) {
const fullFetch = addresses.length === 1;
const fetchedAddresses = uniq(addresses.concat(Object.keys(accounts)));
return Promise
.all(addresses.map((addr) => fetchAccount(addr, api, fullFetch)))
.all(fetchedAddresses.map((addr) => fetchAccount(addr, api, fullFetch)))
.then((accountsBalances) => {
const balances = {};
addresses.forEach((addr, idx) => {
fetchedAddresses.forEach((addr, idx) => {
balances[addr] = accountsBalances[idx];
});

View File

@ -28,3 +28,4 @@ export statusReducer from './statusReducer';
export blockchainReducer from './blockchainReducer';
export compilerReducer from './compilerReducer';
export snackbarReducer from './snackbarReducer';
export walletReducer from './walletReducer';

View File

@ -17,11 +17,45 @@
import { isEqual } from 'lodash';
import { fetchBalances } from './balancesActions';
import { attachWallets } from './walletActions';
export function personalAccountsInfo (accountsInfo) {
const accounts = {};
const contacts = {};
const contracts = {};
const wallets = {};
Object.keys(accountsInfo || {})
.map((address) => Object.assign({}, accountsInfo[address], { address }))
.filter((account) => !account.meta.deleted)
.forEach((account) => {
if (account.uuid) {
accounts[account.address] = account;
} else if (account.meta.wallet) {
account.wallet = true;
wallets[account.address] = account;
} else if (account.meta.contract) {
contracts[account.address] = account;
} else {
contacts[account.address] = account;
}
});
return (dispatch) => {
const data = {
accountsInfo,
accounts, contacts, contracts, wallets
};
dispatch(_personalAccountsInfo(data));
dispatch(attachWallets(wallets));
};
}
function _personalAccountsInfo (data) {
return {
type: 'personalAccountsInfo',
accountsInfo
...data
};
}

View File

@ -25,28 +25,14 @@ const initialState = {
hasContacts: false,
contracts: {},
hasContracts: false,
wallet: {},
hasWallets: false,
visibleAccounts: []
};
export default handleActions({
personalAccountsInfo (state, action) {
const { accountsInfo } = action;
const accounts = {};
const contacts = {};
const contracts = {};
Object.keys(accountsInfo || {})
.map((address) => Object.assign({}, accountsInfo[address], { address }))
.filter((account) => !account.meta.deleted)
.forEach((account) => {
if (account.uuid) {
accounts[account.address] = account;
} else if (account.meta.contract) {
contracts[account.address] = account;
} else {
contacts[account.address] = account;
}
});
const { accountsInfo, accounts, contacts, contracts, wallets } = action;
return Object.assign({}, state, {
accountsInfo,
@ -55,7 +41,9 @@ export default handleActions({
contacts,
hasContacts: Object.keys(contacts).length !== 0,
contracts,
hasContracts: Object.keys(contracts).length !== 0
hasContracts: Object.keys(contracts).length !== 0,
wallets,
hasWallets: Object.keys(wallets).length !== 0
});
},

View File

@ -0,0 +1,503 @@
// 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 { isEqual, uniq } from 'lodash';
import Contract from '~/api/contract';
import { wallet as WALLET_ABI } from '~/contracts/abi';
import { bytesToHex, toHex } from '~/api/util/format';
import { ERROR_CODES } from '~/api/transport/error';
import { MAX_GAS_ESTIMATION } from '../../util/constants';
import WalletsUtils from '~/util/wallets';
import { newError } from '~/ui/Errors/actions';
const UPDATE_OWNERS = 'owners';
const UPDATE_REQUIRE = 'require';
const UPDATE_DAILYLIMIT = 'dailylimit';
const UPDATE_TRANSACTIONS = 'transactions';
const UPDATE_CONFIRMATIONS = 'confirmations';
export function confirmOperation (address, owner, operation) {
return modifyOperation('confirm', address, owner, operation);
}
export function revokeOperation (address, owner, operation) {
return modifyOperation('revoke', address, owner, operation);
}
function modifyOperation (method, address, owner, operation) {
return (dispatch, getState) => {
const { api } = getState();
const contract = new Contract(api, WALLET_ABI).at(address);
const options = {
from: owner,
gas: MAX_GAS_ESTIMATION
};
const values = [ operation ];
dispatch(setOperationPendingState(address, operation, true));
contract.instance[method]
.estimateGas(options, values)
.then((gas) => {
options.gas = gas;
return contract.instance[method].postTransaction(options, values);
})
.then((requestId) => {
return api
.pollMethod('parity_checkRequest', requestId)
.catch((e) => {
dispatch(setOperationPendingState(address, operation, false));
if (e.code === ERROR_CODES.REQUEST_REJECTED) {
return;
}
throw e;
});
})
.catch((error) => {
dispatch(setOperationPendingState(address, operation, false));
dispatch(newError(error));
});
};
}
export function attachWallets (_wallets) {
return (dispatch, getState) => {
const { wallet, api } = getState();
const prevAddresses = wallet.walletsAddresses;
const nextAddresses = Object.keys(_wallets).map((a) => a.toLowerCase()).sort();
if (isEqual(prevAddresses, nextAddresses)) {
return;
}
if (wallet.filterSubId) {
api.eth.uninstallFilter(wallet.filterSubId);
}
if (nextAddresses.length === 0) {
return dispatch(updateWallets({ wallets: {}, walletsAddresses: [], filterSubId: null }));
}
const filterOptions = {
fromBlock: 0,
toBlock: 'latest',
address: nextAddresses
};
api.eth
.newFilter(filterOptions)
.then((filterId) => {
dispatch(updateWallets({ wallets: _wallets, walletsAddresses: nextAddresses, filterSubId: filterId }));
})
.catch((error) => {
if (process.env.NODE_ENV === 'production') {
console.error('walletActions::attachWallets', error);
} else {
throw error;
}
});
fetchWalletsInfo(Object.keys(_wallets))(dispatch, getState);
};
}
export function load (api) {
return (dispatch, getState) => {
const contract = new Contract(api, WALLET_ABI);
dispatch(setWalletContract(contract));
api.subscribe('eth_blockNumber', (error) => {
if (error) {
if (process.env.NODE_ENV === 'production') {
return console.error('[eth_blockNumber] walletActions::load', error);
} else {
throw error;
}
}
const { filterSubId } = getState().wallet;
if (!filterSubId) {
return;
}
api.eth
.getFilterChanges(filterSubId)
.then((logs) => contract.parseEventLogs(logs))
.then((logs) => {
parseLogs(logs)(dispatch, getState);
})
.catch((error) => {
if (process.env.NODE_ENV === 'production') {
return console.error('[getFilterChanges] walletActions::load', error);
} else {
throw error;
}
});
});
};
}
function fetchWalletsInfo (updates) {
return (dispatch, getState) => {
if (Array.isArray(updates)) {
const _updates = updates.reduce((updates, address) => {
updates[address] = {
[ UPDATE_OWNERS ]: true,
[ UPDATE_REQUIRE ]: true,
[ UPDATE_DAILYLIMIT ]: true,
[ UPDATE_CONFIRMATIONS ]: true,
[ UPDATE_TRANSACTIONS ]: true,
address
};
return updates;
}, {});
return fetchWalletsInfo(_updates)(dispatch, getState);
}
const { api } = getState();
const _updates = Object.values(updates);
Promise
.all(_updates.map((update) => {
const contract = new Contract(api, WALLET_ABI).at(update.address);
return fetchWalletInfo(contract, update, getState);
}))
.then((updates) => {
dispatch(updateWalletsDetails(updates));
})
.catch((error) => {
if (process.env.NODE_ENV === 'production') {
return console.error('walletAction::fetchWalletsInfo', error);
} else {
throw error;
}
});
};
}
function fetchWalletInfo (contract, update, getState) {
const promises = [];
if (update[UPDATE_OWNERS]) {
promises.push(fetchWalletOwners(contract));
}
if (update[UPDATE_REQUIRE]) {
promises.push(fetchWalletRequire(contract));
}
if (update[UPDATE_DAILYLIMIT]) {
promises.push(fetchWalletDailylimit(contract));
}
if (update[UPDATE_TRANSACTIONS]) {
promises.push(fetchWalletTransactions(contract));
}
return Promise
.all(promises)
.then((updates) => {
if (update[UPDATE_CONFIRMATIONS]) {
const ownersUpdate = updates.find((u) => u.key === UPDATE_OWNERS);
const transactionsUpdate = updates.find((u) => u.key === UPDATE_TRANSACTIONS);
const owners = ownersUpdate && ownersUpdate.value || null;
const transactions = transactionsUpdate && transactionsUpdate.value || null;
return fetchWalletConfirmations(contract, owners, transactions, getState)
.then((update) => {
updates.push(update);
return updates;
});
}
return updates;
})
.then((updates) => {
const wallet = { address: update.address };
updates.forEach((update) => {
wallet[update.key] = update.value;
});
return wallet;
});
}
function fetchWalletTransactions (contract) {
return WalletsUtils
.fetchTransactions(contract)
.then((transactions) => {
return {
key: UPDATE_TRANSACTIONS,
value: transactions
};
});
}
function fetchWalletOwners (contract) {
return WalletsUtils
.fetchOwners(contract)
.then((value) => {
return {
key: UPDATE_OWNERS,
value
};
});
}
function fetchWalletRequire (contract) {
return WalletsUtils
.fetchRequire(contract)
.then((value) => {
return {
key: UPDATE_REQUIRE,
value
};
});
}
function fetchWalletDailylimit (contract) {
return WalletsUtils
.fetchDailylimit(contract)
.then((value) => {
return {
key: UPDATE_DAILYLIMIT,
value
};
});
}
function fetchWalletConfirmations (contract, _owners = null, _transactions = null, getState) {
const walletInstance = contract.instance;
const wallet = getState().wallet.wallets[contract.address];
const owners = _owners || (wallet && wallet.owners) || null;
const transactions = _transactions || (wallet && wallet.transactions) || null;
return walletInstance
.ConfirmationNeeded
.getAllLogs()
.then((logs) => {
return logs.sort((logA, logB) => {
const comp = logA.blockNumber.comparedTo(logB.blockNumber);
if (comp !== 0) {
return comp;
}
return logA.transactionIndex.comparedTo(logB.transactionIndex);
});
})
.then((logs) => {
return logs.map((log) => ({
initiator: log.params.initiator.value,
to: log.params.to.value,
data: log.params.data.value,
value: log.params.value.value,
operation: bytesToHex(log.params.operation.value),
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
confirmedBy: []
}));
})
.then((confirmations) => {
if (confirmations.length === 0) {
return confirmations;
}
if (transactions) {
const operations = transactions
.filter((t) => t.operation)
.map((t) => t.operation);
return confirmations.filter((confirmation) => {
return !operations.includes(confirmation.operation);
});
}
return confirmations;
})
.then((confirmations) => {
if (confirmations.length === 0) {
return confirmations;
}
const operations = confirmations.map((conf) => conf.operation);
return Promise
.all(operations.map((op) => fetchOperationConfirmations(contract, op, owners)))
.then((confirmedBys) => {
confirmations.forEach((_, index) => {
confirmations[index].confirmedBy = confirmedBys[index];
});
return confirmations;
});
})
.then((confirmations) => {
return {
key: UPDATE_CONFIRMATIONS,
value: confirmations
};
});
}
function fetchOperationConfirmations (contract, operation, owners = null) {
if (!owners) {
console.warn('[fetchOperationConfirmations] try to provide the owners for the Wallet', contract.address);
}
const walletInstance = contract.instance;
const promise = owners
? Promise.resolve({ value: owners })
: fetchWalletOwners(contract);
return promise
.then((result) => {
const owners = result.value;
return Promise
.all(owners.map((owner) => walletInstance.hasConfirmed.call({}, [ operation, owner ])))
.then((data) => {
return owners.filter((owner, index) => data[index]);
});
});
}
function parseLogs (logs) {
return (dispatch, getState) => {
if (!logs || logs.length === 0) {
return;
}
const { wallet } = getState();
const { contract } = wallet;
const walletInstance = contract.instance;
const signatures = {
OwnerChanged: toHex(walletInstance.OwnerChanged.signature),
OwnerAdded: toHex(walletInstance.OwnerAdded.signature),
OwnerRemoved: toHex(walletInstance.OwnerRemoved.signature),
RequirementChanged: toHex(walletInstance.RequirementChanged.signature),
Confirmation: toHex(walletInstance.Confirmation.signature),
Revoke: toHex(walletInstance.Revoke.signature),
Deposit: toHex(walletInstance.Deposit.signature),
SingleTransact: toHex(walletInstance.SingleTransact.signature),
MultiTransact: toHex(walletInstance.MultiTransact.signature),
ConfirmationNeeded: toHex(walletInstance.ConfirmationNeeded.signature)
};
const updates = {};
logs.forEach((log) => {
const { address, topics } = log;
const eventSignature = toHex(topics[0]);
const prev = updates[address] || { address };
switch (eventSignature) {
case signatures.OwnerChanged:
case signatures.OwnerAdded:
case signatures.OwnerRemoved:
updates[address] = {
...prev,
[ UPDATE_OWNERS ]: true
};
return;
case signatures.RequirementChanged:
updates[address] = {
...prev,
[ UPDATE_REQUIRE ]: true
};
return;
case signatures.Confirmation:
case signatures.Revoke:
const operation = log.params.operation.value;
updates[address] = {
...prev,
[ UPDATE_CONFIRMATIONS ]: uniq(
(prev.operations || []).concat(operation)
)
};
return;
case signatures.Deposit:
case signatures.SingleTransact:
case signatures.MultiTransact:
updates[address] = {
...prev,
[ UPDATE_TRANSACTIONS ]: true
};
return;
case signatures.ConfirmationNeeded:
const op = log.params.operation.value;
updates[address] = {
...prev,
[ UPDATE_CONFIRMATIONS ]: uniq(
(prev.operations || []).concat(op)
)
};
return;
}
});
fetchWalletsInfo(updates)(dispatch, getState);
};
}
function setOperationPendingState (address, operation, isPending) {
return {
type: 'setOperationPendingState',
address, operation, isPending
};
}
function updateWalletsDetails (wallets) {
return {
type: 'updateWalletsDetails',
wallets
};
}
function setWalletContract (contract) {
return {
type: 'setWalletContract',
contract
};
}
function updateWallets (data) {
return {
type: 'updateWallets',
...data
};
}

View File

@ -0,0 +1,89 @@
// 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 { handleActions } from 'redux-actions';
const initialState = {
wallets: {},
walletsAddresses: [],
filterSubId: null,
contract: null
};
export default handleActions({
updateWallets: (state, action) => {
const { wallets, walletsAddresses, filterSubId } = action;
return {
...state,
wallets, walletsAddresses, filterSubId
};
},
updateWalletsDetails: (state, action) => {
const { wallets } = action;
const prevWallets = state.wallets;
const nextWallets = { ...prevWallets };
Object.values(wallets).forEach((wallet) => {
const prevWallet = prevWallets[wallet.address] || {};
nextWallets[wallet.address] = {
instance: (state.contract && state.contract.instance) || null,
...prevWallet,
...wallet
};
});
return {
...state,
wallets: nextWallets
};
},
setWalletContract: (state, action) => {
const { contract } = action;
return {
...state,
contract
};
},
setOperationPendingState: (state, action) => {
const { address, operation, isPending } = action;
const { wallets } = state;
const wallet = { ...wallets[address] };
wallet.confirmations = wallet.confirmations.map((conf) => {
if (conf.operation === operation) {
conf.pending = isPending;
}
return conf;
});
return {
...state,
wallets: {
...wallets,
[ address ]: wallet
}
};
}
}, initialState);

View File

@ -17,7 +17,7 @@
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import { apiReducer, balancesReducer, blockchainReducer, compilerReducer, imagesReducer, personalReducer, signerReducer, statusReducer as nodeStatusReducer, snackbarReducer } from './providers';
import { apiReducer, balancesReducer, blockchainReducer, compilerReducer, imagesReducer, personalReducer, signerReducer, statusReducer as nodeStatusReducer, snackbarReducer, walletReducer } from './providers';
import certificationsReducer from './providers/certifications/reducer';
import errorReducer from '~/ui/Errors/reducers';
@ -40,6 +40,7 @@ export default function () {
nodeStatus: nodeStatusReducer,
personal: personalReducer,
signer: signerReducer,
snackbar: snackbarReducer
snackbar: snackbarReducer,
wallet: walletReducer
});
}

View File

@ -19,6 +19,8 @@ import { applyMiddleware, createStore } from 'redux';
import initMiddleware from './middleware';
import initReducers from './reducers';
import { load as loadWallet } from './providers/walletActions';
import {
Balances as BalancesProvider,
Personal as PersonalProvider,
@ -40,5 +42,7 @@ export default function (api) {
new SignerProvider(store, api).start();
new StatusProvider(store, api).start();
store.dispatch(loadWallet(api));
return store;
}

View File

@ -1,7 +1,25 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import ActionDone from 'material-ui/svg-icons/action/done';
import ContentClear from 'material-ui/svg-icons/content/clear';
import { nodeOrStringProptype } from '~/util/proptypes';
import Button from '../Button';
import Modal from '../Modal';
@ -15,9 +33,7 @@ export default class ConfirmDialog extends Component {
iconDeny: PropTypes.node,
labelConfirm: PropTypes.string,
labelDeny: PropTypes.string,
title: PropTypes.oneOfType([
PropTypes.node, PropTypes.string
]).isRequired,
title: nodeOrStringProptype().isRequired,
visible: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired,
onDeny: PropTypes.func.isRequired

View File

@ -16,17 +16,15 @@
import React, { Component, PropTypes } from 'react';
import { nodeOrStringProptype } from '~/util/proptypes';
import styles from './title.css';
export default class Title extends Component {
static propTypes = {
className: PropTypes.string,
title: PropTypes.oneOfType([
PropTypes.string, PropTypes.node
]),
byline: PropTypes.oneOfType([
PropTypes.string, PropTypes.node
])
title: nodeOrStringProptype(),
byline: nodeOrStringProptype()
}
state = {

View File

@ -17,6 +17,8 @@
import React, { Component, PropTypes } from 'react';
import { Card } from 'material-ui/Card';
import { nodeOrStringProptype } from '~/util/proptypes';
import Title from './Title';
import styles from './container.css';
@ -28,9 +30,7 @@ export default class Container extends Component {
compact: PropTypes.bool,
light: PropTypes.bool,
style: PropTypes.object,
title: PropTypes.oneOfType([
PropTypes.string, PropTypes.node
])
title: nodeOrStringProptype()
}
render () {

View File

@ -60,7 +60,8 @@ class Errors extends Component {
flexDirection: 'row',
lineHeight: '1.5em',
padding: '0.75em 0',
alignItems: 'center'
alignItems: 'center',
justifyContent: 'space-between'
} }
/>
);

View File

@ -33,6 +33,7 @@ export default class AddressSelect extends Component {
accounts: PropTypes.object,
contacts: PropTypes.object,
contracts: PropTypes.object,
wallets: PropTypes.object,
label: PropTypes.string,
hint: PropTypes.string,
error: PropTypes.string,
@ -49,8 +50,8 @@ export default class AddressSelect extends Component {
}
entriesFromProps (props = this.props) {
const { accounts, contacts, contracts } = props;
const entries = Object.assign({}, accounts || {}, contacts || {}, contracts || {});
const { accounts, contacts, contracts, wallets } = props;
const entries = Object.assign({}, accounts || {}, wallets || {}, contacts || {}, contracts || {});
return entries;
}

View File

@ -120,7 +120,7 @@ export default class AutoComplete extends Component {
switch (keycode(event)) {
case 'down':
const { menu } = muiAutocomplete.refs;
menu.handleKeyDown(event);
menu && menu.handleKeyDown(event);
this.setState({ fakeBlur: true });
break;
@ -133,7 +133,7 @@ export default class AutoComplete extends Component {
const e = new CustomEvent('down');
e.which = 40;
muiAutocomplete.handleKeyDown(e);
muiAutocomplete && muiAutocomplete.handleKeyDown(e);
break;
}
}

View File

@ -66,7 +66,8 @@ export default class Input extends Component {
PropTypes.number, PropTypes.string
]),
min: PropTypes.any,
max: PropTypes.any
max: PropTypes.any,
style: PropTypes.object
};
static defaultProps = {
@ -74,11 +75,12 @@ export default class Input extends Component {
readOnly: false,
allowCopy: false,
hideUnderline: false,
floatCopy: false
floatCopy: false,
style: {}
}
state = {
value: this.props.value || ''
value: typeof this.props.value === 'undefined' ? '' : this.props.value
}
componentWillReceiveProps (newProps) {
@ -89,7 +91,8 @@ export default class Input extends Component {
render () {
const { value } = this.state;
const { children, className, hideUnderline, disabled, error, label, hint, multiLine, rows, type, min, max } = this.props;
const { children, className, hideUnderline, disabled, error, label } = this.props;
const { hint, multiLine, rows, type, min, max, style } = this.props;
const readOnly = this.props.readOnly || disabled;
@ -105,7 +108,7 @@ export default class Input extends Component {
}
return (
<div className={ styles.container }>
<div className={ styles.container } style={ style }>
{ this.renderCopyButton() }
<TextField
autoComplete='off'

View File

@ -21,12 +21,30 @@
.input input {
padding-left: 48px !important;
box-sizing: border-box;
&.small {
padding-left: 40px !important;
}
}
.inputEmpty input {
padding-left: 0 !important;
}
.small {
.input input {
padding-left: 40px !important;
}
.icon,
.iconDisabled {
img {
height: 24px;
width: 24px;
}
}
}
.icon,
.iconDisabled {
position: absolute;
@ -35,6 +53,14 @@
&.noLabel {
top: 10px;
}
&.noCopy {
left: 5px;
}
&.noUnderline {
top: 0;
}
}
.icon {

View File

@ -36,22 +36,38 @@ class InputAddress extends Component {
tokens: PropTypes.object,
text: PropTypes.bool,
onChange: PropTypes.func,
onSubmit: PropTypes.func
onSubmit: PropTypes.func,
hideUnderline: PropTypes.bool,
allowCopy: PropTypes.bool,
small: PropTypes.bool
};
static defaultProps = {
allowCopy: true,
hideUnderline: false,
small: false
};
render () {
const { className, disabled, error, label, hint, value, text, onSubmit, accountsInfo, tokens } = this.props;
const { className, disabled, error, label, hint, value, text } = this.props;
const { small, allowCopy, hideUnderline, onSubmit, accountsInfo, tokens } = this.props;
const account = accountsInfo[value] || tokens[value];
const hasAccount = account && (!account.meta || !account.meta.deleted);
const hasAccount = account && !(account.meta && account.meta.deleted);
const icon = this.renderIcon();
const classes = [ className ];
classes.push(!icon ? styles.inputEmpty : styles.input);
const containerClasses = [ styles.container ];
if (small) {
containerClasses.push(styles.small);
}
return (
<div className={ styles.container }>
<div className={ containerClasses.join(' ') }>
<Input
className={ classes.join(' ') }
disabled={ disabled }
@ -61,7 +77,8 @@ class InputAddress extends Component {
value={ text && hasAccount ? account.name : value }
onChange={ this.handleInputChange }
onSubmit={ onSubmit }
allowCopy={ disabled ? value : false }
allowCopy={ allowCopy && (disabled ? value : false) }
hideUnderline={ hideUnderline }
/>
{ icon }
</div>
@ -69,7 +86,7 @@ class InputAddress extends Component {
}
renderIcon () {
const { value, disabled, label } = this.props;
const { value, disabled, label, allowCopy, hideUnderline } = this.props;
if (!value || !value.length || !util.isAddressValid(value)) {
return null;
@ -81,6 +98,14 @@ class InputAddress extends Component {
classes.push(styles.noLabel);
}
if (!allowCopy) {
classes.push(styles.noCopy);
}
if (hideUnderline) {
classes.push(styles.noUnderline);
}
return (
<div className={ classes.join(' ') }>
<IdentityIcon

View File

@ -25,6 +25,7 @@ class InputAddressSelect extends Component {
accounts: PropTypes.object.isRequired,
contacts: PropTypes.object.isRequired,
contracts: PropTypes.object.isRequired,
wallets: PropTypes.object.isRequired,
error: PropTypes.string,
label: PropTypes.string,
hint: PropTypes.string,
@ -33,7 +34,7 @@ class InputAddressSelect extends Component {
};
render () {
const { accounts, contacts, contracts, label, hint, error, value, onChange } = this.props;
const { accounts, contacts, contracts, wallets, label, hint, error, value, onChange } = this.props;
return (
<AddressSelect
@ -41,6 +42,7 @@ class InputAddressSelect extends Component {
accounts={ accounts }
contacts={ contacts }
contracts={ contracts }
wallets={ wallets }
error={ error }
label={ label }
hint={ hint }
@ -51,12 +53,13 @@ class InputAddressSelect extends Component {
}
function mapStateToProps (state) {
const { accounts, contacts, contracts } = state.personal;
const { accounts, contacts, contracts, wallets } = state.personal;
return {
accounts,
contacts,
contracts
contracts,
wallets
};
}

View File

@ -16,6 +16,8 @@
import React, { Component, PropTypes } from 'react';
import { nodeOrStringProptype } from '~/util/proptypes';
import Input from '../Input';
import styles from './inputInline.css';
@ -33,9 +35,7 @@ export default class InputInline extends Component {
value: PropTypes.oneOfType([
PropTypes.number, PropTypes.string
]),
static: PropTypes.oneOfType([
PropTypes.node, PropTypes.string
])
static: nodeOrStringProptype()
}
state = {

View File

@ -37,7 +37,10 @@ export default class RadioButtons extends Component {
render () {
const { value, values } = this.props;
const index = parseInt(value);
const index = Number.isNaN(parseInt(value))
? values.findIndex((val) => val.key === value)
: parseInt(value);
const selectedValue = typeof value !== 'object' ? values[index] : value;
const key = this.getKey(selectedValue, index);

View File

@ -34,12 +34,20 @@ export default class TypedInput extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
accounts: PropTypes.object.isRequired,
param: PropTypes.object.isRequired,
accounts: PropTypes.object,
error: PropTypes.any,
value: PropTypes.any,
label: PropTypes.string
label: PropTypes.string,
hint: PropTypes.string,
min: PropTypes.number,
max: PropTypes.number
};
static defaultProps = {
min: null,
max: null
};
render () {
@ -89,16 +97,22 @@ export default class TypedInput extends Component {
};
const style = {
width: 32,
height: 32,
width: 24,
height: 24,
padding: 0
};
const plusStyle = {
...style,
backgroundColor: 'rgba(255, 255, 255, 0.25)',
borderRadius: '50%'
};
return (
<div>
<div style={ { marginTop: '0.75em' } }>
<IconButton
iconStyle={ iconStyle }
style={ style }
style={ plusStyle }
onTouchTap={ this.onAddField }
>
<AddIcon />
@ -144,26 +158,29 @@ export default class TypedInput extends Component {
}
renderNumber () {
const { label, value, error, param } = this.props;
const { label, value, error, param, hint, min, max } = this.props;
return (
<Input
label={ label }
hint={ hint }
value={ value }
error={ error }
onSubmit={ this.onSubmit }
onChange={ this.onChange }
type='number'
min={ param.signed ? null : 0 }
min={ min !== null ? min : (param.signed ? null : 0) }
max={ max !== null ? max : null }
/>
);
}
renderDefault () {
const { label, value, error } = this.props;
const { label, value, error, hint } = this.props;
return (
<Input
label={ label }
hint={ hint }
value={ value }
error={ error }
onSubmit={ this.onSubmit }
@ -172,12 +189,13 @@ export default class TypedInput extends Component {
}
renderAddress () {
const { accounts, label, value, error } = this.props;
const { accounts, label, value, error, hint } = this.props;
return (
<InputAddressSelect
accounts={ accounts }
label={ label }
hint={ hint }
value={ value }
error={ error }
onChange={ this.onChange }
@ -187,7 +205,7 @@ export default class TypedInput extends Component {
}
renderBoolean () {
const { label, value, error } = this.props;
const { label, value, error, hint } = this.props;
const boolitems = ['false', 'true'].map((bool) => {
return (
@ -204,6 +222,7 @@ export default class TypedInput extends Component {
return (
<Select
label={ label }
hint={ hint }
value={ value ? 'true' : 'false' }
error={ error }
onChange={ this.onChangeBool }

View File

@ -457,6 +457,14 @@ class MethodDecoding extends Component {
return;
}
const { signature, paramdata } = api.util.decodeCallData(input);
this.setState({ methodSignature: signature, methodParams: paramdata });
if (!signature || signature === CONTRACT_CREATE || transaction.creates) {
this.setState({ isDeploy: true });
return;
}
if (contractAddress === '0x') {
return;
}
@ -472,14 +480,6 @@ class MethodDecoding extends Component {
return;
}
const { signature, paramdata } = api.util.decodeCallData(input);
this.setState({ methodSignature: signature, methodParams: paramdata });
if (!signature || signature === CONTRACT_CREATE || transaction.creates) {
this.setState({ isDeploy: true });
return;
}
return Contracts.get()
.signatureReg
.lookup(signature)

View File

@ -18,6 +18,8 @@ import React, { Component, PropTypes } from 'react';
import { LinearProgress } from 'material-ui';
import { Step, Stepper, StepLabel } from 'material-ui/Stepper';
import { nodeOrStringProptype } from '~/util/proptypes';
import styles from '../modal.css';
export default class Title extends Component {
@ -26,9 +28,7 @@ export default class Title extends Component {
current: PropTypes.number,
steps: PropTypes.array,
waiting: PropTypes.array,
title: React.PropTypes.oneOfType([
PropTypes.node, PropTypes.string
])
title: nodeOrStringProptype()
}
render () {

View File

@ -19,6 +19,8 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Dialog } from 'material-ui';
import { nodeOrStringProptype } from '~/util/proptypes';
import Container from '../Container';
import Title from './Title';
@ -42,9 +44,7 @@ class Modal extends Component {
current: PropTypes.number,
waiting: PropTypes.array,
steps: PropTypes.array,
title: PropTypes.oneOfType([
PropTypes.node, PropTypes.string
]),
title: nodeOrStringProptype(),
visible: PropTypes.bool.isRequired,
settings: PropTypes.object.isRequired
}

View File

@ -39,14 +39,20 @@ export class TxRow extends Component {
address: PropTypes.string.isRequired,
isTest: PropTypes.bool.isRequired,
block: PropTypes.object
block: PropTypes.object,
historic: PropTypes.bool,
className: PropTypes.string
};
static defaultProps = {
historic: true
};
render () {
const { tx, address, isTest } = this.props;
const { tx, address, isTest, historic, className } = this.props;
return (
<tr>
<tr className={ className || '' }>
{ this.renderBlockNumber(tx.blockNumber) }
{ this.renderAddress(tx.from) }
<td className={ styles.transaction }>
@ -64,7 +70,7 @@ export class TxRow extends Component {
{ this.renderAddress(tx.to) }
<td className={ styles.method }>
<MethodDecoding
historic
historic={ historic }
address={ address }
transaction={ tx } />
</td>

31
js/src/util/proptypes.js Normal file
View File

@ -0,0 +1,31 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { PropTypes } from 'react';
export function nullableProptype (type) {
return PropTypes.oneOfType([
PropTypes.oneOf([ null ]),
type
]);
}
export function nodeOrStringProptype () {
return PropTypes.oneOfType([
PropTypes.node,
PropTypes.string
]);
}

107
js/src/util/wallets.js Normal file
View File

@ -0,0 +1,107 @@
// 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 { range } from 'lodash';
import { bytesToHex, toHex } from '~/api/util/format';
export default class WalletsUtils {
static fetchRequire (walletContract) {
return walletContract.instance.m_required.call();
}
static fetchOwners (walletContract) {
const walletInstance = walletContract.instance;
return walletInstance
.m_numOwners.call()
.then((mNumOwners) => {
return Promise.all(range(mNumOwners.toNumber()).map((idx) => walletInstance.getOwner.call({}, [ idx ])));
});
}
static fetchDailylimit (walletContract) {
const walletInstance = walletContract.instance;
return Promise
.all([
walletInstance.m_dailyLimit.call(),
walletInstance.m_spentToday.call(),
walletInstance.m_lastDay.call()
])
.then(([ limit, spent, last ]) => ({
limit, spent, last
}));
}
static fetchTransactions (walletContract) {
const walletInstance = walletContract.instance;
const signatures = {
single: toHex(walletInstance.SingleTransact.signature),
multi: toHex(walletInstance.MultiTransact.signature),
deposit: toHex(walletInstance.Deposit.signature)
};
return walletContract
.getAllLogs({
topics: [ [ signatures.single, signatures.multi, signatures.deposit ] ]
})
.then((logs) => {
return logs.sort((logA, logB) => {
const comp = logB.blockNumber.comparedTo(logA.blockNumber);
if (comp !== 0) {
return comp;
}
return logB.transactionIndex.comparedTo(logA.transactionIndex);
});
})
.then((logs) => {
const transactions = logs.map((log) => {
const signature = toHex(log.topics[0]);
const value = log.params.value.value;
const from = signature === signatures.deposit
? log.params['_from'].value
: walletContract.address;
const to = signature === signatures.deposit
? walletContract.address
: log.params.to.value;
const transaction = {
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
from, to, value
};
if (log.params.operation) {
transaction.operation = bytesToHex(log.params.operation.value);
}
if (log.params.data) {
transaction.data = log.params.data.value;
}
return transaction;
});
return transactions;
});
}
}

View File

@ -25,16 +25,23 @@ import styles from './header.css';
export default class Header extends Component {
static contextTypes = {
api: PropTypes.object
}
};
static propTypes = {
account: PropTypes.object,
balance: PropTypes.object
}
balance: PropTypes.object,
className: PropTypes.string,
children: PropTypes.node
};
static defaultProps = {
className: '',
children: null
};
render () {
const { api } = this.context;
const { account, balance } = this.props;
const { account, balance, className, children } = this.props;
const { address, meta, uuid } = account;
if (!account) {
@ -46,7 +53,7 @@ export default class Header extends Component {
: <div className={ styles.uuidline }>uuid: { uuid }</div>;
return (
<div>
<div className={ className }>
<Container>
<IdentityIcon
address={ address } />
@ -74,6 +81,7 @@ export default class Header extends Component {
dappsUrl={ api.dappsUrl }
/>
</div>
{ children }
</Container>
</div>
);
@ -88,6 +96,10 @@ export default class Header extends Component {
const { txCount } = balance;
if (!txCount) {
return null;
}
return (
<div className={ styles.infoline }>
{ txCount.toFormat() } outgoing transactions

View File

@ -21,7 +21,7 @@ import ContentAdd from 'material-ui/svg-icons/content/add';
import { uniq, isEqual } from 'lodash';
import List from './List';
import { CreateAccount } from '~/modals';
import { CreateAccount, CreateWallet } from '~/modals';
import { Actionbar, ActionbarExport, ActionbarSearch, ActionbarSort, Button, Page, Tooltip } from '~/ui';
import { setVisibleAccounts } from '~/redux/providers/personalActions';
@ -34,15 +34,18 @@ class Accounts extends Component {
static propTypes = {
setVisibleAccounts: PropTypes.func.isRequired,
accounts: PropTypes.object.isRequired,
hasAccounts: PropTypes.bool.isRequired,
wallets: PropTypes.object.isRequired,
hasWallets: PropTypes.bool.isRequired,
accounts: PropTypes.object,
hasAccounts: PropTypes.bool,
balances: PropTypes.object
}
state = {
addressBook: false,
newDialog: false,
newWalletDialog: false,
sortOrder: '',
searchValues: [],
searchTokens: [],
@ -58,8 +61,8 @@ class Accounts extends Component {
}
componentWillReceiveProps (nextProps) {
const prevAddresses = Object.keys(this.props.accounts);
const nextAddresses = Object.keys(nextProps.accounts);
const prevAddresses = Object.keys({ ...this.props.accounts, ...this.props.wallets });
const nextAddresses = Object.keys({ ...nextProps.accounts, ...nextProps.wallets });
if (prevAddresses.length !== nextAddresses.length || !isEqual(prevAddresses.sort(), nextAddresses.sort())) {
this.setVisibleAccounts(nextProps);
@ -71,8 +74,8 @@ class Accounts extends Component {
}
setVisibleAccounts (props = this.props) {
const { accounts, setVisibleAccounts } = props;
const addresses = Object.keys(accounts);
const { accounts, wallets, setVisibleAccounts } = props;
const addresses = Object.keys({ ...accounts, ...wallets });
setVisibleAccounts(addresses);
}
@ -80,17 +83,24 @@ class Accounts extends Component {
return (
<div className={ styles.accounts }>
{ this.renderNewDialog() }
{ this.renderNewWalletDialog() }
{ this.renderActionbar() }
{ this.state.show ? this.renderAccounts() : this.renderLoading() }
<Page>
<Tooltip
className={ styles.accountTooltip }
text='your accounts are visible for easy access, allowing you to edit the meta information, make transfers, view transactions and fund the account'
/>
{ this.renderWallets() }
{ this.renderAccounts() }
</Page>
</div>
);
}
renderLoading () {
const { accounts } = this.props;
const loadings = ((accounts && Object.keys(accounts)) || []).map((_, idx) => (
renderLoading (object) {
const loadings = ((object && Object.keys(object)) || []).map((_, idx) => (
<div key={ idx } className={ styles.loading }>
<div />
</div>
@ -104,11 +114,14 @@ class Accounts extends Component {
}
renderAccounts () {
if (!this.state.show) {
return this.renderLoading(this.props.accounts);
}
const { accounts, hasAccounts, balances } = this.props;
const { searchValues, sortOrder } = this.state;
return (
<Page>
<List
search={ searchValues }
accounts={ accounts }
@ -116,10 +129,27 @@ class Accounts extends Component {
empty={ !hasAccounts }
order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken } />
<Tooltip
className={ styles.accountTooltip }
text='your accounts are visible for easy access, allowing you to edit the meta information, make transfers, view transactions and fund the account' />
</Page>
);
}
renderWallets () {
if (!this.state.show) {
return this.renderLoading(this.props.wallets);
}
const { wallets, hasWallets, balances } = this.props;
const { searchValues, sortOrder } = this.state;
return (
<List
link='wallet'
search={ searchValues }
accounts={ wallets }
balances={ balances }
empty={ !hasWallets }
order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken }
/>
);
}
@ -160,6 +190,12 @@ class Accounts extends Component {
label='new account'
onClick={ this.onNewAccountClick } />,
<Button
key='newWallet'
icon={ <ContentAdd /> }
label='new wallet'
onClick={ this.onNewWalletClick } />,
<ActionbarExport
key='exportAccounts'
content={ accounts }
@ -198,6 +234,22 @@ class Accounts extends Component {
);
}
renderNewWalletDialog () {
const { accounts } = this.props;
const { newWalletDialog } = this.state;
if (!newWalletDialog) {
return null;
}
return (
<CreateWallet
accounts={ accounts }
onClose={ this.onNewWalletClose }
/>
);
}
onAddSearchToken = (token) => {
const { searchTokens } = this.state;
const newSearchTokens = uniq([].concat(searchTokens, token));
@ -210,21 +262,33 @@ class Accounts extends Component {
});
}
onNewWalletClick = () => {
this.setState({
newWalletDialog: !this.state.newWalletDialog
});
}
onNewAccountClose = () => {
this.onNewAccountClick();
}
onNewWalletClose = () => {
this.onNewWalletClick();
}
onNewAccountUpdate = () => {
}
}
function mapStateToProps (state) {
const { accounts, hasAccounts } = state.personal;
const { accounts, hasAccounts, wallets, hasWallets } = state.personal;
const { balances } = state.balances;
return {
accounts,
hasAccounts,
wallets,
hasWallets,
balances
};
}

View File

@ -19,7 +19,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { ConfirmDialog, IdentityIcon, IdentityName } from '~/ui';
import { newError } from '../../../redux/actions';
import { newError } from '~/redux/actions';
import styles from '../address.css';
@ -27,16 +27,17 @@ class Delete extends Component {
static contextTypes = {
api: PropTypes.object.isRequired,
router: PropTypes.object
}
};
static propTypes = {
route: PropTypes.string.isRequired,
address: PropTypes.string,
account: PropTypes.object,
route: PropTypes.string.isRequired,
visible: PropTypes.bool,
onClose: PropTypes.func,
newError: PropTypes.func
}
};
render () {
const { account, visible } = this.props;

View File

@ -28,6 +28,7 @@ import imagesEthcoreBlock from '../../../../assets/images/parity-logo-white-no-t
const TABMAP = {
accounts: 'account',
wallet: 'account',
addresses: 'address',
apps: 'app',
contracts: 'contract',

View File

@ -23,7 +23,7 @@ import ContentCreate from 'material-ui/svg-icons/content/create';
import EyeIcon from 'material-ui/svg-icons/image/remove-red-eye';
import ContentClear from 'material-ui/svg-icons/content/clear';
import { newError } from '../../redux/actions';
import { newError } from '~/redux/actions';
import { setVisibleAccounts } from '~/redux/providers/personalActions';
import { EditMeta, ExecuteContract } from '~/modals';

View File

@ -19,7 +19,7 @@ import { action, computed, observable, transaction } from 'mobx';
import store from 'store';
import Contracts from '~/contracts';
import { hashToImageUrl } from '../../redux/util';
import { hashToImageUrl } from '~/redux/util';
import builtinApps from './builtin.json';

View File

@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react';
import ReactTooltip from 'react-tooltip';
import { MethodDecoding } from '../../../../ui';
import { MethodDecoding } from '~/ui';
import * as tUtil from '../util/transaction';
import Account from '../Account';

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