Merge branch 'master' into auth-bft

This commit is contained in:
keorn 2016-12-07 10:41:07 +01:00
commit da030fed51
84 changed files with 4239 additions and 352 deletions

11
Cargo.lock generated
View File

@ -298,6 +298,7 @@ dependencies = [
"ethcore-ipc 1.4.0",
"ethcore-ipc-codegen 1.4.0",
"ethcore-ipc-nano 1.4.0",
"ethcore-network 1.5.0",
"ethcore-util 1.5.0",
"ethjson 0.1.0",
"ethkey 0.2.0",
@ -841,7 +842,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)",
@ -853,7 +854,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)",
@ -864,7 +865,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)",
@ -879,7 +880,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)",
@ -1271,7 +1272,7 @@ dependencies = [
[[package]]
name = "parity-ui-precompiled"
version = "1.4.0"
source = "git+https://github.com/ethcore/js-precompiled.git#3cf6c68b7d08be71d12ff142e255a42043e50c75"
source = "git+https://github.com/ethcore/js-precompiled.git#57f5bf943f24cf761ba58c1bea35a845e0b12414"
dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

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;
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;
extern crate ethcore_ipc as ipc;
extern crate rlp;
extern crate time;
#[macro_use]
extern crate log;
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 block = {
let rlp = try!(data.at(1));
(try!(rlp.val_at(0)), try!(rlp.val_at(1)))
};
let req = request::Headers {
block: {
let rlp = try!(data.at(1));
(try!(rlp.val_at(0)), try!(rlp.val_at(1)))
},
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))
@ -503,4 +821,86 @@ impl NetworkProtocolHandler for LightProtocol {
_ => warn!(target: "les", "received timeout on unknown token {}", timer),
}
}
}
// 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,15 +64,105 @@ 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>;
/// 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

@ -52,7 +52,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;
@ -1311,6 +1311,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 {
@ -1395,32 +1402,60 @@ 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() {
use tests::helpers::*;
use std::thread;
use std::time::Duration;
use std::sync::atomic::{AtomicBool, Ordering};
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)
}
let client = generate_dummy_client(0);
let genesis = client.chain_info().best_block_hash;
let (new_hash, new_block) = get_good_dummy_block_hash();
let go = {
// Separate thread uncommited transaction
let go = Arc::new(AtomicBool::new(false));
let go_thread = go.clone();
let another_client = client.reference().clone();
thread::spawn(move || {
let mut batch = DBTransaction::new(&*another_client.chain.read().db().clone());
another_client.chain.read().insert_block(&mut batch, &new_block, Vec::new());
go_thread.store(true, Ordering::SeqCst);
});
go
};
while !go.load(Ordering::SeqCst) { thread::park_timeout(Duration::from_millis(5)); }
assert!(client.tree_route(&genesis, &new_hash).is_none());
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;
let (new_hash, new_block) = get_good_dummy_block_hash();
let go = {
// Separate thread uncommited transaction
let go = Arc::new(AtomicBool::new(false));
let go_thread = go.clone();
let another_client = client.reference().clone();
thread::spawn(move || {
let mut batch = DBTransaction::new(&*another_client.chain.read().db().clone());
another_client.chain.read().insert_block(&mut batch, &new_block, Vec::new());
go_thread.store(true, Ordering::SeqCst);
});
go
};
while !go.load(Ordering::SeqCst) { thread::park_timeout(Duration::from_millis(5)); }
assert!(client.tree_route(&genesis, &new_hash).is_none());
}
}

View File

@ -25,18 +25,21 @@ mod client;
pub use self::client::*;
pub use self::config::{Mode, ClientConfig, 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;
@ -671,4 +672,11 @@ impl BlockChainClient for TestBlockChainClient {
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.
@ -253,10 +254,15 @@ pub trait BlockChainClient : Sync + Send {
/// 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,
@ -274,4 +280,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

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

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

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

View File

@ -209,8 +209,10 @@ export default class Contract {
_bindFunction = (func) => {
func.call = (options, values = []) => {
const callData = this._encodeOptions(func, this._addOptionsTo(options), values);
return this._api.eth
.call(this._encodeOptions(func, this._addOptionsTo(options), values))
.call(callData)
.then((encoded) => func.decodeOutput(encoded))
.then((tokens) => tokens.map((token) => token.value))
.then((returns) => returns.length === 1 ? returns[0] : returns);

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

@ -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,111 @@
// 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 } from '../../../ui';
import { parseAbiType } from '../../../util/abi';
export default class WalletDetails extends Component {
static propTypes = {
accounts: PropTypes.object.isRequired,
wallet: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired
};
render () {
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[]') }
/>
<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') }
/>
<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') }
/>
</Form>
);
}
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,85 @@
// 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
};
render () {
const { address, required, daylimit, name } = this.props;
return (
<CompletedStep>
<div><code>{ name }</code> has been deployed at</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 } = this.props;
return [].concat(account, owners).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

@ -0,0 +1,39 @@
/* 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;
}
}

View File

@ -0,0 +1,182 @@
// 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 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 }
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 }
/>
);
default:
case 'DETAILS':
return (
<WalletDetails
accounts={ accounts }
wallet={ this.store.wallet }
errors={ this.store.errors }
onChange={ this.store.onChange }
/>
);
}
}
renderDialogActions () {
const { step, hasErrors, rejected, onCreate } = 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 createBtn = (
<Button
icon={ <NavigationArrowForward /> }
label='Create'
disabled={ hasErrors }
onClick={ onCreate }
/>
);
if (rejected) {
return [ closeBtn ];
}
switch (step) {
case 'DEPLOYMENT':
return [ closeBtn, sendingBtn ];
case 'INFO':
return [ doneBtn ];
default:
case 'DETAILS':
return [ cancelBtn, createBtn ];
}
}
onClose = () => {
this.props.onClose();
}
}

View File

@ -0,0 +1,199 @@
// 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 { ERRORS, validateUint, validateAddress, validateName } from '../../util/validation';
import { ERROR_CODES } from '../../api/transport/error';
import { wallet as walletAbi } from '../../contracts/abi';
import { wallet as walletCode } from '../../contracts/code';
const STEPS = {
DETAILS: { title: 'wallet details' },
DEPLOYMENT: { title: 'wallet deployment', waiting: true },
INFO: { title: 'wallet informaton' }
};
const STEPS_KEYS = Object.keys(STEPS);
export default class CreateWalletStore {
@observable step = null;
@observable rejected = false;
@observable deployState = null;
@observable deployError = null;
@observable txhash = null;
@observable wallet = {
account: '',
address: '',
owners: [],
required: 1,
daylimit: 0,
name: '',
description: ''
};
@observable errors = {
account: null,
owners: null,
required: null,
daylimit: null,
name: ERRORS.invalidName
};
@computed get stage () {
return STEPS_KEYS.findIndex((k) => k === this.step);
}
@computed get hasErrors () {
return !!Object.values(this.errors).find((e) => !!e);
}
steps = Object.values(STEPS).map((s) => s.title);
waiting = Object.values(STEPS)
.map((s, idx) => ({ idx, waiting: s.waiting }))
.filter((s) => s.waiting)
.map((s) => s.idx);
constructor (api, accounts) {
this.api = api;
this.step = STEPS_KEYS[0];
this.wallet.account = Object.values(accounts)[0].address;
}
@action onChange = (_wallet) => {
const newWallet = Object.assign({}, this.wallet, _wallet);
const { errors, wallet } = this.validateWallet(newWallet);
transaction(() => {
this.wallet = wallet;
this.errors = errors;
});
}
@action onCreate = () => {
if (this.hasErrors) {
return;
}
this.step = 'DEPLOYMENT';
const { account, owners, required, daylimit, name, description } = this.wallet;
const options = {
data: walletCode,
from: account
};
this.api
.newContract(walletAbi)
.deploy(options, [ owners, required, daylimit ], this.onDeploymentState)
.then((address) => {
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
})
])
.then(() => {
transaction(() => {
this.wallet.address = address;
this.step = 'INFO';
});
});
})
.catch((error) => {
if (error.code === ERROR_CODES.REQUEST_REJECTED) {
this.rejected = true;
return;
}
console.error('error deploying contract', error);
this.deployError = error;
});
}
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;
}
}
validateWallet = (_wallet) => {
const accountValidation = validateAddress(_wallet.account);
const requiredValidation = validateUint(_wallet.required);
const daylimitValidation = validateUint(_wallet.daylimit);
const nameValidation = validateName(_wallet.name);
const errors = {
account: accountValidation.addressError,
required: requiredValidation.valueError,
daylimit: daylimitValidation.valueError,
name: nameValidation.nameError
};
const wallet = {
..._wallet,
account: accountValidation.address,
required: requiredValidation.value,
daylimit: daylimitValidation.value,
name: nameValidation.name
};
return { errors, wallet };
}
}

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

@ -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;
@ -207,7 +239,11 @@ export default class Details extends Component {
}
renderTokenSelect () {
const { balance, images, tag } = this.props;
const { balance, images, tag, wallet } = this.props;
if (wallet) {
return null;
}
return (
<TokenSelect
@ -223,6 +259,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

@ -33,28 +33,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 +71,9 @@ export default class TransferStore {
gasLimit = null;
onClose = null;
isWallet = false;
wallet = null;
@computed get steps () {
const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC);
@ -73,7 +85,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 +101,28 @@ 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 } = props;
this.account = account;
this.balance = balance;
this.gasLimit = gasLimit;
this.onClose = onClose;
this.isWallet = account && account.wallet;
if (this.isWallet) {
this.wallet = props.wallet;
}
if (senders) {
this.senderError = ERRORS.requireSender;
}
}
@action onNext = () => {
@ -133,6 +158,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 +193,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';
@ -250,6 +277,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 +324,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;
@ -361,74 +404,70 @@ 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);
}
const options = {
from: account.address,
to: recipient,
gas,
gasPrice,
value: this.api.util.toWei(value || 0)
};
estimateGas () {
const { options, values } = this._getTransferParams(true);
return this._getTransferMethod(true).estimateGas(options, values);
}
if (data && data.length) {
options.data = data;
_getTransferMethod (gas = false) {
const { isEth, isWallet } = this;
if (isEth && !isWallet) {
return gas ? this.api.eth : this.api.parity;
}
return this.api.parity.postTransaction(options);
}
_sendToken () {
const { account, balance } = this;
const { gas, gasPrice, recipient, value, tag } = this;
const token = balance.tokens.find((balance) => balance.token.tag === tag).token;
return token.contract.instance.transfer
.postTransaction({
from: account.address,
to: token.address,
gas,
gasPrice
}, [
recipient,
new BigNumber(value).mul(token.format).toFixed(0)
]);
}
_estimateGasToken () {
const { account, balance } = this;
const { recipient, value, tag } = this;
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;
if (isWallet) {
return this.wallet.instance.execute;
}
return this.api.eth.estimateGas(options);
return this.token.contract.instance.transfer;
}
_getTransferParams (gas = false) {
const { isEth, isWallet } = this;
const to = (isEth && !isWallet) ? this.recipient
: (this.isWallet ? this.wallet.address : this.token.address);
const options = {
from: this.sender || this.account.address,
to
};
if (!gas) {
options.gas = this.gas;
options.gasPrice = this.gasPrice;
} else {
options.gas = MAX_GAS_ESTIMATION;
}
if (isEth && !isWallet) {
options.value = this.api.util.toWei(this.value || 0);
if (this.data && this.data.length) {
options.data = this.data;
}
return { options, values: [] };
}
const values = isWallet
? [
this.recipient,
this.api.util.toWei(this.value || 0),
this.data || ''
]
: [
this.recipient,
new BigNumber(this.value || 0).mul(this.token.format).toFixed(0)
];
return { options, values };
}
_validatePositiveNumber (num) {

View File

@ -26,6 +26,7 @@ import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forwa
import { newError } from '~/ui/Errors/actions';
import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash } from '~/ui';
import { nullableProptype } from '~/util/proptypes';
import Details from './Details';
import Extras from './Extras';
@ -45,8 +46,10 @@ class Transfer extends Component {
images: PropTypes.object.isRequired,
account: PropTypes.object,
senders: nullableProptype(PropTypes.object),
balance: PropTypes.object,
balances: PropTypes.object,
wallet: PropTypes.object,
onClose: PropTypes.func
}
@ -135,9 +138,9 @@ class Transfer extends Component {
}
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 +149,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 +257,28 @@ class Transfer extends Component {
}
}
function mapStateToProps (state) {
const { gasLimit } = state.nodeStatus;
return { gasLimit };
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, wallet, senders };
};
}
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

@ -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,567 @@
// 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, range } 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 { 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) {
const walletInstance = contract.instance;
const signatures = {
single: toHex(walletInstance.SingleTransact.signature),
multi: toHex(walletInstance.MultiTransact.signature),
deposit: toHex(walletInstance.Deposit.signature)
};
return contract
.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
: contract.address;
const to = signature === signatures.deposit
? contract.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 {
key: UPDATE_TRANSACTIONS,
value: transactions
};
});
}
function fetchWalletOwners (contract) {
const walletInstance = contract.instance;
return walletInstance
.m_numOwners.call()
.then((mNumOwners) => {
return Promise.all(range(mNumOwners.toNumber()).map((idx) => walletInstance.getOwner.call({}, [ idx ])));
})
.then((value) => {
return {
key: UPDATE_OWNERS,
value
};
});
}
function fetchWalletRequire (contract) {
const walletInstance = contract.instance;
return walletInstance
.m_required.call()
.then((value) => {
return {
key: UPDATE_REQUIRE,
value
};
});
}
function fetchWalletDailylimit (contract) {
const walletInstance = contract.instance;
return Promise
.all([
walletInstance.m_dailyLimit.call(),
walletInstance.m_spentToday.call(),
walletInstance.m_lastDay.call()
])
.then((values) => {
return {
key: UPDATE_DAILYLIMIT,
value: {
limit: values[0],
spent: values[1],
last: values[2]
}
};
});
}
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

@ -78,7 +78,7 @@ export default class Input extends Component {
}
state = {
value: this.props.value || ''
value: typeof this.props.value === 'undefined' ? '' : this.props.value
}
componentWillReceiveProps (newProps) {

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

@ -34,12 +34,13 @@ 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
};
render () {
@ -144,11 +145,12 @@ export default class TypedInput extends Component {
}
renderNumber () {
const { label, value, error, param } = this.props;
const { label, value, error, param, hint } = this.props;
return (
<Input
label={ label }
hint={ hint }
value={ value }
error={ error }
onSubmit={ this.onSubmit }
@ -159,11 +161,12 @@ export default class TypedInput extends Component {
}
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 +175,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 +191,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 +208,7 @@ export default class TypedInput extends Component {
return (
<Select
label={ label }
hint={ hint }
value={ value ? 'true' : 'false' }
error={ error }
onChange={ this.onChangeBool }

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
]);
}

View File

@ -88,6 +88,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,17 @@ class Accounts extends Component {
return (
<div className={ styles.accounts }>
{ this.renderNewDialog() }
{ this.renderNewWalletDialog() }
{ this.renderActionbar() }
{ this.state.show ? this.renderAccounts() : this.renderLoading() }
{ this.renderAccounts() }
{ this.renderWallets() }
</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,6 +107,10 @@ 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;
@ -123,6 +130,29 @@ class Accounts extends Component {
);
}
renderWallets () {
if (!this.state.show) {
return this.renderLoading(this.props.wallets);
}
const { wallets, hasWallets, balances } = this.props;
const { searchValues, sortOrder } = this.state;
return (
<Page>
<List
link='wallet'
search={ searchValues }
accounts={ wallets }
balances={ balances }
empty={ !hasWallets }
order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken }
/>
</Page>
);
}
renderSearchButton () {
const onChange = (searchTokens, searchValues) => {
this.setState({ searchTokens, searchValues });
@ -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

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

@ -0,0 +1,372 @@
// 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 { LinearProgress, MenuItem, IconMenu } from 'material-ui';
import ReactTooltip from 'react-tooltip';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { confirmOperation, revokeOperation } from '../../../redux/providers/walletActions';
import { bytesToHex } from '../../../api/util/format';
import { Container, InputAddress, Button, IdentityIcon } from '../../../ui';
import { TxRow } from '../../../ui/TxList/txList';
import styles from '../wallet.css';
import txListStyles from '../../../ui/TxList/txList.css';
class WalletConfirmations extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
accounts: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
isTest: PropTypes.bool.isRequired,
owners: PropTypes.array.isRequired,
require: PropTypes.object.isRequired,
confirmOperation: PropTypes.func.isRequired,
revokeOperation: PropTypes.func.isRequired,
confirmations: PropTypes.array
};
static defaultProps = {
confirmations: []
};
render () {
return (
<div>
<Container title='Pending Confirmations'>
{ this.renderConfirmations() }
</Container>
</div>
);
}
renderConfirmations () {
const { confirmations, ...others } = this.props;
if (!confirmations) {
return null;
}
if (confirmations.length === 0) {
return (
<div>
<p>No transactions needs confirmation right now.</p>
</div>
);
}
return confirmations.map((confirmation) => (
<WalletConfirmation
key={ confirmation.operation }
confirmation={ confirmation }
{ ...others }
/>
));
}
}
function mapStateToProps (state) {
const { accounts } = state.personal;
return { accounts };
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
confirmOperation,
revokeOperation
}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(WalletConfirmations);
class WalletConfirmation extends Component {
static propTypes = {
accounts: PropTypes.object.isRequired,
confirmation: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
isTest: PropTypes.bool.isRequired,
owners: PropTypes.array.isRequired,
require: PropTypes.object.isRequired,
confirmOperation: PropTypes.func.isRequired,
revokeOperation: PropTypes.func.isRequired
};
state = {
openConfirm: false,
openRevoke: false
};
render () {
const { confirmation } = this.props;
const confirmationsRows = [];
const className = styles.light;
const txRow = this.renderTransactionRow(confirmation, className);
const detailsRow = this.renderConfirmedBy(confirmation, className);
const progressRow = this.renderProgress(confirmation, className);
const actionsRow = this.renderActions(confirmation, className);
confirmationsRows.push(progressRow);
confirmationsRows.push(detailsRow);
confirmationsRows.push(txRow);
confirmationsRows.push(actionsRow);
return (
<div className={ styles.confirmationContainer }>
<table className={ [ txListStyles.transactions, styles.confirmations ].join(' ') }>
<tbody>
{ confirmationsRows }
</tbody>
</table>
{ this.renderPending() }
</div>
);
}
renderPending () {
const { pending } = this.props.confirmation;
if (!pending) {
return null;
}
return (
<div className={ styles.pendingOverlay } />
);
}
handleOpenConfirm = () => {
this.setState({
openConfirm: true
});
}
handleCloseConfirm = () => {
this.setState({
openConfirm: false
});
}
handleOpenRevoke = () => {
this.setState({
openRevoke: true
});
}
handleCloseRevoke = () => {
this.setState({
openRevoke: false
});
}
handleConfirm = (e, item) => {
const { confirmOperation, confirmation, address } = this.props;
const owner = item.props.value;
confirmOperation(address, owner, confirmation.operation);
}
handleRevoke = (e, item) => {
const { revokeOperation, confirmation, address } = this.props;
const owner = item.props.value;
revokeOperation(address, owner, confirmation.operation);
}
renderActions (confirmation, className) {
const { owners, accounts } = this.props;
const { operation, confirmedBy, pending } = confirmation;
const { openConfirm, openRevoke } = this.state;
const addresses = Object.keys(accounts);
const possibleConfirm = owners
.filter((owner) => addresses.includes(owner))
.filter((owner) => !confirmedBy.includes(owner));
const possibleRevoke = owners
.filter((owner) => addresses.includes(owner))
.filter((owner) => confirmedBy.includes(owner));
const confirmButton = (
<Button
onClick={ this.handleOpenConfirm }
label='Confirm As...'
disabled={ pending || possibleConfirm.length === 0 }
/>
);
const revokeButton = (
<Button
onClick={ this.handleOpenRevoke }
label='Revoke As...'
disabled={ pending || possibleRevoke.length === 0 }
/>
);
return (
<tr key={ `actions_${operation}` } className={ className }>
<td />
<td colSpan={ 3 }>
<div className={ styles.actions }>
<IconMenu
iconButtonElement={ confirmButton }
open={ openConfirm }
onRequestChange={ this.handleCloseConfirm }
onItemTouchTap={ this.handleConfirm }
>
{ possibleConfirm.map((address) => this.renderAccountItem(address)) }
</IconMenu>
<IconMenu
iconButtonElement={ revokeButton }
open={ openRevoke }
onRequestChange={ this.handleCloseRevoke }
onItemTouchTap={ this.handleRevoke }
>
{ possibleRevoke.map((address) => this.renderAccountItem(address)) }
</IconMenu>
</div>
</td>
<td />
</tr>
);
}
renderAccountItem (address) {
const account = this.props.accounts[address];
return (
<MenuItem value={ address } key={ address }>
<div className={ styles.accountItem }>
<IdentityIcon
inline center
address={ address }
/>
<span>{ account.name.toUpperCase() || account.address }</span>
</div>
</MenuItem>
);
}
renderProgress (confirmation) {
const { require } = this.props;
const { operation, confirmedBy, pending } = confirmation;
const style = { borderRadius: 0 };
return (
<tr key={ `prog_${operation}` }>
<td colSpan={ 5 } style={ { padding: 0, paddingTop: '1em' } }>
<div
data-tip
data-for={ `tooltip_${operation}` }
data-effect='solid'
>
{
pending
? (
<LinearProgress
key={ `pending_${operation}` }
mode='indeterminate'
style={ style }
/>
)
: (
<LinearProgress
key={ `unpending_${operation}` }
mode='determinate'
min={ 0 }
max={ require.toNumber() }
value={ confirmedBy.length }
style={ style }
/>
)
}
</div>
<ReactTooltip id={ `tooltip_${operation}` }>
Confirmed by { confirmedBy.length }/{ require.toNumber() } owners
</ReactTooltip>
</td>
</tr>
);
}
renderTransactionRow (confirmation, className) {
const { address, isTest } = this.props;
const { operation, transactionHash, blockNumber, value, to, data } = confirmation;
return (
<TxRow
className={ className }
key={ operation }
tx={ {
hash: transactionHash,
blockNumber: blockNumber,
from: address,
to: to,
value: value,
input: bytesToHex(data)
} }
address={ address }
isTest={ isTest }
historic={ false }
/>
);
}
renderConfirmedBy (confirmation, className) {
const { operation, confirmedBy } = confirmation;
const confirmed = confirmedBy.map((owner) => (
<InputAddress
key={ owner }
value={ owner }
allowCopy={ false }
hideUnderline
disabled
small
text
/>
));
return (
<tr key={ `details_${operation}` } className={ className }>
<td colSpan={ 5 } style={ { padding: 0 } }>
<div
data-tip
data-for={ `tooltip_${operation}` }
data-effect='solid'
className={ styles.confirmed }
>
{ confirmed }
</div>
</td>
</tr>
);
}
}

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 './confirmations';

View File

@ -0,0 +1,102 @@
// 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 moment from 'moment';
import { Container, InputAddress } from '../../../ui';
import styles from '../wallet.css';
export default class WalletDetails extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
owners: PropTypes.array,
require: PropTypes.object,
dailylimit: PropTypes.object
};
render () {
return (
<div className={ styles.details }>
<Container title='Owners'>
{ this.renderOwners() }
</Container>
<Container title='Details'>
{ this.renderDetails() }
</Container>
</div>
);
}
renderOwners () {
const { owners } = this.props;
if (!owners) {
return null;
}
const ownersList = owners.map((address) => (
<InputAddress
key={ address }
value={ address }
disabled
text
/>
));
return (
<div>
{ ownersList }
</div>
);
}
renderDetails () {
const { require, dailylimit } = this.props;
const { api } = this.context;
if (!dailylimit || !dailylimit.limit) {
return null;
}
const limit = api.util.fromWei(dailylimit.limit).toFormat(3);
const spent = api.util.fromWei(dailylimit.spent).toFormat(3);
const date = moment(dailylimit.last.toNumber() * 24 * 3600 * 1000);
return (
<div>
<p>
<span>This wallet requires at least</span>
<span className={ styles.detail }>{ require.toFormat() } owners</span>
<span>to validate any action (transactions, modifications).</span>
</p>
<p>
<span className={ styles.detail }>{ spent }<span className={ styles.eth } /></span>
<span>has been spent today, out of</span>
<span className={ styles.detail }>{ limit }<span className={ styles.eth } /></span>
<span>set as the daily limit, which has been reset on</span>
<span className={ styles.detail }>{ date.format('LL') }</span>
</p>
</div>
);
}
}

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 './details';

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 './transactions';

View File

@ -0,0 +1,85 @@
// 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 { bytesToHex } from '../../../api/util/format';
import { Container } from '../../../ui';
import { TxRow } from '../../../ui/TxList/txList';
import txListStyles from '../../../ui/TxList/txList.css';
export default class WalletTransactions extends Component {
static propTypes = {
address: PropTypes.string.isRequired,
isTest: PropTypes.bool.isRequired,
transactions: PropTypes.array
};
static defaultProps = {
transactions: []
};
render () {
return (
<div>
<Container title='Transactions'>
{ this.renderTransactions() }
</Container>
</div>
);
}
renderTransactions () {
const { address, isTest, transactions } = this.props;
if (!transactions) {
return null;
}
if (transactions.length === 0) {
return (
<div>
<p>No transactions has been sent.</p>
</div>
);
}
const txRows = transactions.map((transaction) => {
const { transactionHash, blockNumber, from, to, value, data } = transaction;
return (
<TxRow
key={ transactionHash }
tx={ {
hash: transactionHash,
input: data && bytesToHex(data) || '',
blockNumber, from, to, value
} }
address={ address }
isTest={ isTest }
/>
);
});
return (
<table className={ txListStyles.transactions }>
<tbody>
{ txRows }
</tbody>
</table>
);
}
}

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 './wallet';

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/>.
*/
.details {
margin: 0;
display: flex;
flex-direction: row;
> * {
flex: 1;
margin: 0.125em;
height: auto;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
}
.detail {
font-size: 1.125em;
color: white;
margin: 0 0.375em;
line-height: 1.5em;
&:first-child {
margin-left: 0;
}
}
.eth:after {
content: 'ETH';
font-size: 0.75em;
margin-left: 0.125em;
}
.progressText {
text-align: center;
margin: 0.75em 0 0.25em;
}
.confirmed {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: 0.75em 0.5em 0;
}
.confirmations {
tr {
&:nth-child(even) {
background-color: initial;
}
&.light {
background-color: rgba(255, 255, 255, 0.04);
}
&.dark {
background-color: transparent;
}
}
}
.actions {
display: flex;
flex-direction: row;
justify-content: space-around;
}
.accountItem {
display: flex;
flex-direction: row;
align-items: center;
}
.confirmationContainer {
position: relative;
}
.pendingOverlay {
position: absolute;
top: 1em;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(255, 255, 255, 0.1);
}

View File

@ -0,0 +1,273 @@
// 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 { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import ContentCreate from 'material-ui/svg-icons/content/create';
import ContentSend from 'material-ui/svg-icons/content/send';
import { EditMeta, Transfer } from '../../modals';
import { Actionbar, Button, Page, Loading } from '../../ui';
import Header from '../Account/Header';
import WalletDetails from './Details';
import WalletConfirmations from './Confirmations';
import WalletTransactions from './Transactions';
import { setVisibleAccounts } from '../../redux/providers/personalActions';
import styles from './wallet.css';
class WalletContainer extends Component {
static propTypes = {
isTest: PropTypes.any
};
render () {
const { isTest, ...others } = this.props;
if (isTest !== false && isTest !== true) {
return (
<Loading size={ 4 } />
);
}
return (
<Wallet isTest={ isTest } { ...others } />
);
}
}
class Wallet extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
setVisibleAccounts: PropTypes.func.isRequired,
images: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
wallets: PropTypes.object.isRequired,
wallet: PropTypes.object.isRequired,
balances: PropTypes.object.isRequired,
isTest: PropTypes.bool.isRequired
};
state = {
showEditDialog: false,
showTransferDialog: false
};
componentDidMount () {
this.setVisibleAccounts();
}
componentWillReceiveProps (nextProps) {
const prevAddress = this.props.address;
const nextAddress = nextProps.address;
if (prevAddress !== nextAddress) {
this.setVisibleAccounts(nextProps);
}
}
componentWillUnmount () {
this.props.setVisibleAccounts([]);
}
setVisibleAccounts (props = this.props) {
const { address, setVisibleAccounts } = props;
const addresses = [ address ];
setVisibleAccounts(addresses);
}
render () {
const { wallets, balances, address } = this.props;
const wallet = (wallets || {})[address];
const balance = (balances || {})[address];
if (!wallet) {
return null;
}
return (
<div className={ styles.wallet }>
{ this.renderEditDialog(wallet) }
{ this.renderTransferDialog() }
{ this.renderActionbar() }
<Page>
<Header
account={ wallet }
balance={ balance }
/>
{ this.renderDetails() }
</Page>
</div>
);
}
renderDetails () {
const { address, isTest, wallet } = this.props;
const { owners, require, dailylimit, confirmations, transactions } = wallet;
if (!isTest || !owners || !require) {
return (
<div style={ { marginTop: '4em' } }>
<Loading size={ 4 } />
</div>
);
}
return [
<WalletDetails
key='details'
owners={ owners }
require={ require }
dailylimit={ dailylimit }
/>,
<WalletConfirmations
key='confirmations'
owners={ owners }
require={ require }
confirmations={ confirmations }
isTest={ isTest }
address={ address }
/>,
<WalletTransactions
key='transactions'
transactions={ transactions }
address={ address }
isTest={ isTest }
/>
];
}
renderActionbar () {
const { address, balances } = this.props;
const balance = balances[address];
const showTransferButton = !!(balance && balance.tokens);
const buttons = [
<Button
key='transferFunds'
icon={ <ContentSend /> }
label='transfer'
disabled={ !showTransferButton }
onClick={ this.onTransferClick } />,
<Button
key='editmeta'
icon={ <ContentCreate /> }
label='edit'
onClick={ this.onEditClick } />
];
return (
<Actionbar
title='Wallet Management'
buttons={ buttons } />
);
}
renderEditDialog (wallet) {
const { showEditDialog } = this.state;
if (!showEditDialog) {
return null;
}
return (
<EditMeta
account={ wallet }
keys={ ['description', 'passwordHint'] }
onClose={ this.onEditClick } />
);
}
renderTransferDialog () {
const { showTransferDialog } = this.state;
if (!showTransferDialog) {
return null;
}
const { wallets, balances, images, address } = this.props;
const wallet = wallets[address];
const balance = balances[address];
return (
<Transfer
account={ wallet }
balance={ balance }
balances={ balances }
images={ images }
onClose={ this.onTransferClose }
/>
);
}
onEditClick = () => {
this.setState({
showEditDialog: !this.state.showEditDialog
});
}
onTransferClick = () => {
this.setState({
showTransferDialog: !this.state.showTransferDialog
});
}
onTransferClose = () => {
this.onTransferClick();
}
}
function mapStateToProps (_, initProps) {
const { address } = initProps.params;
return (state) => {
const { isTest } = state.nodeStatus;
const { wallets } = state.personal;
const { balances } = state.balances;
const { images } = state;
const wallet = state.wallet.wallets[address] || {};
return {
isTest,
wallets,
balances,
images,
address,
wallet
};
};
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
setVisibleAccounts
}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(WalletContainer);

View File

@ -35,6 +35,11 @@ const SNIPPETS = {
name: 'HumanStandardToken.sol',
description: 'Implementation of the Human Token Contract',
id: 'snippet2', sourcecode: require('raw-loader!../../contracts/snippets/human-standard-token.sol')
},
snippet3: {
name: 'Wallet.sol',
description: 'Implementation of a multisig Wallet',
id: 'snippet3', sourcecode: require('raw-loader!../../contracts/snippets/wallet.sol')
}
};

View File

@ -28,6 +28,7 @@ import ParityBar from './ParityBar';
import Settings, { SettingsBackground, SettingsParity, SettingsProxy, SettingsViews } from './Settings';
import Signer from './Signer';
import Status from './Status';
import Wallet from './Wallet';
export {
Account,
@ -47,5 +48,6 @@ export {
SettingsProxy,
SettingsViews,
Signer,
Status
Status,
Wallet
};

View File

@ -97,11 +97,11 @@ pub trait Trie {
}
/// Query the value of the given key in this trie while recording visited nodes
/// to the given recorder. If the query fails, the nodes passed to the recorder are unspecified.
/// to the given recorder. If the query encounters an error, the nodes passed to the recorder are unspecified.
fn get_recorded<'a, 'b, R: 'b>(&'a self, key: &'b [u8], rec: &'b mut R) -> Result<Option<DBValue>>
where 'a: 'b, R: Recorder;
/// Returns an iterator over elements of trie.
/// Returns a depth-first iterator over the elements of trie.
fn iter<'a>(&'a self) -> Result<Box<TrieIterator<Item = TrieItem> + 'a>>;
}
@ -241,5 +241,5 @@ impl TrieFactory {
}
/// Returns true iff the trie DB is a fat DB (allows enumeration of keys).
pub fn is_fat(&self) -> bool { self.spec == TrieSpec::Fat }
pub fn is_fat(&self) -> bool { self.spec == TrieSpec::Fat }
}

View File

@ -35,7 +35,6 @@ pub struct Record {
/// These are used to record which nodes are visited during a trie query.
/// Inline nodes are not to be recorded, as they are contained within their parent.
pub trait Recorder {
/// Record that the given node has been visited.
///
/// The depth parameter is the depth of the visited node, with the root node having depth 0.
@ -58,6 +57,7 @@ impl Recorder for NoOp {
/// A simple recorder. Does nothing fancy but fulfills the `Recorder` interface
/// properly.
#[derive(Debug)]
pub struct BasicRecorder {
nodes: Vec<Record>,
min_depth: u32,