From 3ff1ca81f484fc17d413d56d7ed931559a652db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Sat, 22 Oct 2016 15:21:41 +0200 Subject: [PATCH 01/77] Missing changes required to make new UI work (#2793) * Getting rid of old dapps * Updating proxypac and allowing home.parity on signer * CORS support for API * Fixing CORS - origin is sent with protocol * Fixing signer with proxy * Fixing grumbles * Fix expect msg [ci:skip] --- Cargo.lock | 31 +-------- dapps/Cargo.toml | 12 +--- dapps/src/api/api.rs | 57 +++++++++++++--- dapps/src/api/cors.rs | 0 dapps/src/api/response.rs | 12 ++-- dapps/src/apps/mod.rs | 75 ++++----------------- dapps/src/handlers/echo.rs | 100 ++-------------------------- dapps/src/handlers/mod.rs | 3 +- dapps/src/lib.rs | 46 ++++++++++--- dapps/src/proxypac.rs | 29 ++++++-- dapps/src/router/host_validation.rs | 2 +- dapps/src/router/mod.rs | 43 +++++++++--- dapps/src/rpc.rs | 2 +- dapps/src/tests/api.rs | 59 ++++++++++++++-- dapps/src/tests/authorization.rs | 6 +- dapps/src/tests/helpers.rs | 24 ++++++- dapps/src/tests/redirection.rs | 8 +-- dapps/src/tests/validation.rs | 31 ++++++++- devtools/src/http_client.rs | 17 +++-- logger/src/lib.rs | 9 ++- signer/src/tests/mod.rs | 50 ++++++++++++-- signer/src/ws_server/session.rs | 28 ++++++-- 22 files changed, 370 insertions(+), 274 deletions(-) create mode 100644 dapps/src/api/cors.rs diff --git a/Cargo.lock b/Cargo.lock index e716b85b8..531557c2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -328,6 +328,7 @@ name = "ethcore-dapps" version = "1.4.0" dependencies = [ "clippy 0.0.90 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "ethabi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "ethcore-devtools 1.4.0", "ethcore-rpc 1.4.0", @@ -340,9 +341,7 @@ dependencies = [ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)", "parity-dapps-glue 1.4.0", - "parity-dapps-home 1.4.0 (git+https://github.com/ethcore/parity-ui.git)", "parity-ui 1.4.0", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", @@ -854,7 +853,7 @@ dependencies = [ [[package]] name = "jsonrpc-http-server" version = "6.1.1" -source = "git+https://github.com/ethcore/jsonrpc-http-server.git#ee72e4778583daf901b5692468fc622f46abecb6" +source = "git+https://github.com/ethcore/jsonrpc-http-server.git#cd6d4cb37d672cc3057aecd0692876f9e85f3ba5" dependencies = [ "hyper 0.9.4 (git+https://github.com/ethcore/hyper)", "jsonrpc-core 3.0.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1175,20 +1174,6 @@ name = "owning_ref" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "parity-dapps" -version = "1.4.0" -source = "git+https://github.com/ethcore/parity-ui.git#8b1c31319228ad4cf9bd4ae740a0b933aa9e19c7" -dependencies = [ - "aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", - "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "quasi 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "quasi_codegen 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "syntex 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)", - "syntex_syntax 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "parity-dapps-glue" version = "1.4.0" @@ -1202,14 +1187,6 @@ dependencies = [ "syntex_syntax 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "parity-dapps-home" -version = "1.4.0" -source = "git+https://github.com/ethcore/parity-ui.git#8b1c31319228ad4cf9bd4ae740a0b933aa9e19c7" -dependencies = [ - "parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)", -] - [[package]] name = "parity-ui" version = "1.4.0" @@ -1884,7 +1861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "ws" version = "0.5.2" -source = "git+https://github.com/ethcore/ws-rs.git?branch=mio-upstream-stable#609b21fdab96c8fffedec8699755ce3bea9454cb" +source = "git+https://github.com/ethcore/ws-rs.git?branch=mio-upstream-stable#e3d21c119350e753fdf4475b8cd88103b2280540" dependencies = [ "bytes 0.4.0-dev (git+https://github.com/carllerche/bytes)", "httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2016,8 +1993,6 @@ dependencies = [ "checksum number_prefix 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "084d05f4bf60621a9ac9bde941a410df548f4de9545f06e5ee9d3aef4b97cd77" "checksum odds 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "b28c06e81b0f789122d415d6394b5fe849bde8067469f4c2980d3cdc10c78ec1" "checksum owning_ref 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8d91377085359426407a287ab16884a0111ba473aa6844ff01d4ec20ce3d75e7" -"checksum parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)" = "" -"checksum parity-dapps-home 1.4.0 (git+https://github.com/ethcore/parity-ui.git)" = "" "checksum parking_lot 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "968f685642555d2f7e202c48b8b11de80569e9bfea817f7f12d7c61aac62d4e6" "checksum parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "dbc5847584161f273e69edc63c1a86254a22f570a0b5dd87aa6f9773f6f7d125" "checksum parking_lot_core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fb1b97670a2ffadce7c397fb80a3d687c4f3060140b885621ef1653d0e5d5068" diff --git a/dapps/Cargo.toml b/dapps/Cargo.toml index 77ed00e19..7ca792d40 100644 --- a/dapps/Cargo.toml +++ b/dapps/Cargo.toml @@ -11,6 +11,7 @@ build = "build.rs" [dependencies] rand = "0.3.14" log = "0.3" +env_logger = "0.3" jsonrpc-core = "3.0" jsonrpc-http-server = { git = "https://github.com/ethcore/jsonrpc-http-server.git" } hyper = { default-features = false, git = "https://github.com/ethcore/hyper" } @@ -23,17 +24,13 @@ serde_macros = { version = "0.8", optional = true } zip = { version = "0.1", default-features = false } ethabi = "0.2.2" linked-hash-map = "0.3" +mime = "0.2" ethcore-devtools = { path = "../devtools" } ethcore-rpc = { path = "../rpc" } ethcore-util = { path = "../util" } fetch = { path = "../util/fetch" } parity-ui = { path = "./ui" } parity-dapps-glue = { path = "./js-glue" } -mime = "0.2" -### DEPRECATED -parity-dapps = { git = "https://github.com/ethcore/parity-ui.git", version = "1.4" } -parity-dapps-home = { git = "https://github.com/ethcore/parity-ui.git", version = "1.4" } -### /DEPRECATED mime_guess = { version = "1.6.1" } clippy = { version = "0.0.90", optional = true} @@ -46,7 +43,4 @@ default = ["serde_codegen"] nightly = ["serde_macros"] dev = ["clippy", "ethcore-rpc/dev", "ethcore-util/dev"] -use-precompiled-js = [ - "parity-ui/use-precompiled-js", - "parity-dapps-home/use-precompiled-js", -] +use-precompiled-js = ["parity-ui/use-precompiled-js"] diff --git a/dapps/src/api/api.rs b/dapps/src/api/api.rs index 80c5e09de..90ab837f5 100644 --- a/dapps/src/api/api.rs +++ b/dapps/src/api/api.rs @@ -15,24 +15,31 @@ // along with Parity. If not, see . use std::sync::Arc; +use unicase::UniCase; use hyper::{server, net, Decoder, Encoder, Next, Control}; +use hyper::header; +use hyper::method::Method; +use hyper::header::AccessControlAllowOrigin; + use api::types::{App, ApiError}; -use api::response::{as_json, as_json_error, ping_response}; +use api::response; +use apps::fetcher::ContentFetcher; + use handlers::extract_url; use endpoint::{Endpoint, Endpoints, Handler, EndpointPath}; -use apps::fetcher::ContentFetcher; +use jsonrpc_http_server::cors; #[derive(Clone)] pub struct RestApi { - local_domain: String, + cors_domains: Option>, endpoints: Arc, fetcher: Arc, } impl RestApi { - pub fn new(local_domain: String, endpoints: Arc, fetcher: Arc) -> Box { + pub fn new(cors_domains: Vec, endpoints: Arc, fetcher: Arc) -> Box { Box::new(RestApi { - local_domain: local_domain, + cors_domains: Some(cors_domains.into_iter().map(AccessControlAllowOrigin::Value).collect()), endpoints: endpoints, fetcher: fetcher, }) @@ -53,6 +60,7 @@ impl Endpoint for RestApi { struct RestApiRouter { api: RestApi, + origin: Option, path: Option, control: Option, handler: Box, @@ -62,9 +70,10 @@ impl RestApiRouter { fn new(api: RestApi, path: EndpointPath, control: Control) -> Self { RestApiRouter { path: Some(path), + origin: None, control: Some(control), api: api, - handler: as_json_error(&ApiError { + handler: response::as_json_error(&ApiError { code: "404".into(), title: "Not Found".into(), detail: "Resource you requested has not been found.".into(), @@ -80,11 +89,40 @@ impl RestApiRouter { _ => None } } + + /// Returns basic headers for a response (it may be overwritten by the handler) + fn response_headers(&self) -> header::Headers { + let mut headers = header::Headers::new(); + headers.set(header::AccessControlAllowCredentials); + headers.set(header::AccessControlAllowMethods(vec![ + Method::Options, + Method::Post, + Method::Get, + ])); + headers.set(header::AccessControlAllowHeaders(vec![ + UniCase("origin".to_owned()), + UniCase("content-type".to_owned()), + UniCase("accept".to_owned()), + ])); + + if let Some(cors_header) = cors::get_cors_header(&self.api.cors_domains, &self.origin) { + headers.set(cors_header); + } + + headers + } } impl server::Handler for RestApiRouter { fn on_request(&mut self, request: server::Request) -> Next { + self.origin = cors::read_origin(&request); + + if let Method::Options = *request.method() { + self.handler = response::empty(); + return Next::write(); + } + let url = extract_url(&request); if url.is_none() { // Just return 404 if we can't parse URL @@ -99,11 +137,11 @@ impl server::Handler for RestApiRouter { let hash = url.path.get(2).map(|v| v.as_str()); // at this point path.app_id contains 'api', adjust it to the hash properly, otherwise // we will try and retrieve 'api' as the hash when doing the /api/content route - if let Some(hash) = hash.clone() { path.app_id = hash.to_owned() } + if let Some(ref hash) = hash { path.app_id = hash.clone().to_owned() } let handler = endpoint.and_then(|v| match v { - "apps" => Some(as_json(&self.api.list_apps())), - "ping" => Some(ping_response(&self.api.local_domain)), + "apps" => Some(response::as_json(&self.api.list_apps())), + "ping" => Some(response::ping()), "content" => self.resolve_content(hash, path, control), _ => None }); @@ -121,6 +159,7 @@ impl server::Handler for RestApiRouter { } fn on_response(&mut self, res: &mut server::Response) -> Next { + *res.headers_mut() = self.response_headers(); self.handler.on_response(res) } diff --git a/dapps/src/api/cors.rs b/dapps/src/api/cors.rs new file mode 100644 index 000000000..e69de29bb diff --git a/dapps/src/api/response.rs b/dapps/src/api/response.rs index 4064490b3..3e8f196ec 100644 --- a/dapps/src/api/response.rs +++ b/dapps/src/api/response.rs @@ -19,6 +19,10 @@ use serde_json; use endpoint::Handler; use handlers::{ContentHandler, EchoHandler}; +pub fn empty() -> Box { + Box::new(ContentHandler::ok("".into(), mime!(Text/Plain))) +} + pub fn as_json(val: &T) -> Box { let json = serde_json::to_string(val) .expect("serialization to string is infallible; qed"); @@ -31,10 +35,6 @@ pub fn as_json_error(val: &T) -> Box { Box::new(ContentHandler::not_found(json, mime!(Application/Json))) } -pub fn ping_response(local_domain: &str) -> Box { - Box::new(EchoHandler::cors(vec![ - format!("http://{}", local_domain), - // Allow CORS calls also for localhost - format!("http://{}", local_domain.replace("127.0.0.1", "localhost")), - ])) +pub fn ping() -> Box { + Box::new(EchoHandler::default()) } diff --git a/dapps/src/apps/mod.rs b/dapps/src/apps/mod.rs index 145c3e820..40ddb7064 100644 --- a/dapps/src/apps/mod.rs +++ b/dapps/src/apps/mod.rs @@ -17,8 +17,7 @@ use endpoint::{Endpoints, Endpoint}; use page::PageEndpoint; use proxypac::ProxyPac; -use parity_dapps::{self, WebApp}; -use parity_dapps_glue::WebApp as NewWebApp; +use parity_dapps::WebApp; mod cache; mod fs; @@ -26,17 +25,14 @@ pub mod urlhint; pub mod fetcher; pub mod manifest; -extern crate parity_dapps_home; extern crate parity_ui; +pub const HOME_PAGE: &'static str = "home"; pub const DAPPS_DOMAIN : &'static str = ".parity"; pub const RPC_PATH : &'static str = "rpc"; pub const API_PATH : &'static str = "api"; pub const UTILS_PATH : &'static str = "parity-utils"; -pub fn main_page() -> &'static str { - "home" -} pub fn redirection_address(using_dapps_domains: bool, app_id: &str) -> String { if using_dapps_domains { format!("http://{}{}/", app_id, DAPPS_DOMAIN) @@ -46,7 +42,7 @@ pub fn redirection_address(using_dapps_domains: bool, app_id: &str) -> String { } pub fn utils() -> Box { - Box::new(PageEndpoint::with_prefix(parity_dapps_home::App::default(), UTILS_PATH.to_owned())) + Box::new(PageEndpoint::with_prefix(parity_ui::App::default(), UTILS_PATH.to_owned())) } pub fn all_endpoints(dapps_path: String, signer_port: Option) -> Endpoints { @@ -54,64 +50,21 @@ pub fn all_endpoints(dapps_path: String, signer_port: Option) -> Endpoints let mut pages = fs::local_endpoints(dapps_path); // NOTE [ToDr] Dapps will be currently embeded on 8180 - pages.insert("ui".into(), Box::new( - PageEndpoint::new_safe_to_embed(NewUi::default(), signer_port) - )); - - pages.insert("proxy".into(), ProxyPac::boxed()); - insert::(&mut pages, "home"); - + insert::(&mut pages, "ui", Embeddable::Yes(signer_port)); + pages.insert("proxy".into(), ProxyPac::boxed(signer_port)); pages } -fn insert(pages: &mut Endpoints, id: &str) { - pages.insert(id.to_owned(), Box::new(PageEndpoint::new(T::default()))); +fn insert(pages: &mut Endpoints, id: &str, embed_at: Embeddable) { + pages.insert(id.to_owned(), Box::new(match embed_at { + Embeddable::Yes(port) => PageEndpoint::new_safe_to_embed(T::default(), port), + Embeddable::No => PageEndpoint::new(T::default()), + })); } -// TODO [ToDr] Temporary wrapper until we get rid of old built-ins. -use std::collections::HashMap; - -struct NewUi { - app: parity_ui::App, - files: HashMap<&'static str, parity_dapps::File>, -} - -impl Default for NewUi { - fn default() -> Self { - let app = parity_ui::App::default(); - let files = { - let mut files = HashMap::new(); - for (k, v) in &app.files { - files.insert(*k, parity_dapps::File { - path: v.path, - content: v.content, - content_type: v.content_type, - }); - } - files - }; - - NewUi { - app: app, - files: files, - } - } -} - -impl WebApp for NewUi { - fn file(&self, path: &str) -> Option<&parity_dapps::File> { - self.files.get(path) - } - - fn info(&self) -> parity_dapps::Info { - let info = self.app.info(); - parity_dapps::Info { - name: info.name, - version: info.version, - author: info.author, - description: info.description, - icon_url: info.icon_url, - } - } +enum Embeddable { + Yes(Option), + #[allow(dead_code)] + No, } diff --git a/dapps/src/handlers/echo.rs b/dapps/src/handlers/echo.rs index 9266de0c6..165ceb171 100644 --- a/dapps/src/handlers/echo.rs +++ b/dapps/src/handlers/echo.rs @@ -17,76 +17,19 @@ //! Echo Handler use std::io::Read; -use hyper::{header, server, Decoder, Encoder, Next}; -use hyper::method::Method; +use hyper::{server, Decoder, Encoder, Next}; use hyper::net::HttpStream; -use unicase::UniCase; use super::ContentHandler; -#[derive(Debug, PartialEq)] -/// Type of Cross-Origin request -enum Cors { - /// Not a Cross-Origin request - no headers needed - No, - /// Cross-Origin request with valid Origin - Allowed(String), - /// Cross-Origin request with invalid Origin - Forbidden, -} - +#[derive(Default)] pub struct EchoHandler { - safe_origins: Vec, content: String, - cors: Cors, handler: Option, } -impl EchoHandler { - - pub fn cors(safe_origins: Vec) -> Self { - EchoHandler { - safe_origins: safe_origins, - content: String::new(), - cors: Cors::Forbidden, - handler: None, - } - } - - fn cors_header(&self, origin: Option) -> Cors { - fn origin_is_allowed(origin: &str, safe_origins: &[String]) -> bool { - for safe in safe_origins { - if origin.starts_with(safe) { - return true; - } - } - false - } - - match origin { - Some(ref origin) if origin_is_allowed(origin, &self.safe_origins) => { - Cors::Allowed(origin.clone()) - }, - None => Cors::No, - _ => Cors::Forbidden, - } - } -} - impl server::Handler for EchoHandler { - fn on_request(&mut self, request: server::Request) -> Next { - let origin = request.headers().get_raw("origin") - .and_then(|list| list.get(0)) - .and_then(|origin| String::from_utf8(origin.clone()).ok()); - - self.cors = self.cors_header(origin); - - // Don't even read the payload if origin is forbidden! - if let Cors::Forbidden = self.cors { - self.handler = Some(ContentHandler::ok(String::new(), mime!(Text/Plain))); - Next::write() - } else { - Next::read() - } + fn on_request(&mut self, _: server::Request) -> Next { + Next::read() } fn on_request_readable(&mut self, decoder: &mut Decoder) -> Next { @@ -104,16 +47,6 @@ impl server::Handler for EchoHandler { } fn on_response(&mut self, res: &mut server::Response) -> Next { - if let Cors::Allowed(ref domain) = self.cors { - let mut headers = res.headers_mut(); - headers.set(header::Allow(vec![Method::Options, Method::Post, Method::Get])); - headers.set(header::AccessControlAllowHeaders(vec![ - UniCase("origin".to_owned()), - UniCase("content-type".to_owned()), - UniCase("accept".to_owned()), - ])); - headers.set(header::AccessControlAllowOrigin::Value(domain.clone())); - } self.handler.as_mut() .expect("handler always set in on_request, which is before now; qed") .on_response(res) @@ -125,28 +58,3 @@ impl server::Handler for EchoHandler { .on_response_writable(encoder) } } - -#[test] -fn should_return_correct_cors_value() { - // given - let safe_origins = vec!["chrome-extension://".to_owned(), "http://localhost:8080".to_owned()]; - let cut = EchoHandler { - safe_origins: safe_origins, - content: String::new(), - cors: Cors::No, - handler: None, - }; - - // when - let res1 = cut.cors_header(Some("http://ethcore.io".into())); - let res2 = cut.cors_header(Some("http://localhost:8080".into())); - let res3 = cut.cors_header(Some("chrome-extension://deadbeefcafe".into())); - let res4 = cut.cors_header(None); - - - // then - assert_eq!(res1, Cors::Forbidden); - assert_eq!(res2, Cors::Allowed("http://localhost:8080".into())); - assert_eq!(res3, Cors::Allowed("chrome-extension://deadbeefcafe".into())); - assert_eq!(res4, Cors::No); -} diff --git a/dapps/src/handlers/mod.rs b/dapps/src/handlers/mod.rs index c4f98fc26..3d96e8a40 100644 --- a/dapps/src/handlers/mod.rs +++ b/dapps/src/handlers/mod.rs @@ -30,6 +30,7 @@ pub use self::fetch::{ContentFetcherHandler, ContentValidator, FetchControl}; use url::Url; use hyper::{server, header, net, uri}; +use signer_address; /// Adds security-related headers to the Response. pub fn add_security_headers(headers: &mut header::Headers, embeddable_at: Option) { @@ -40,7 +41,7 @@ pub fn add_security_headers(headers: &mut header::Headers, embeddable_at: Option if let Some(port) = embeddable_at { headers.set_raw( "X-Frame-Options", - vec![format!("ALLOW-FROM http://127.0.0.1:{}", port).into_bytes()] + vec![format!("ALLOW-FROM http://{}", signer_address(port)).into_bytes()] ); } else { // TODO [ToDr] Should we be more strict here (DENY?)? diff --git a/dapps/src/lib.rs b/dapps/src/lib.rs index 9e9a26ee3..00c8b275e 100644 --- a/dapps/src/lib.rs +++ b/dapps/src/lib.rs @@ -59,19 +59,17 @@ extern crate ethcore_rpc; extern crate ethcore_util as util; extern crate linked_hash_map; extern crate fetch; - +extern crate parity_dapps_glue as parity_dapps; #[macro_use] extern crate log; - #[macro_use] extern crate mime; #[cfg(test)] extern crate ethcore_devtools as devtools; +#[cfg(test)] +extern crate env_logger; -extern crate parity_dapps_glue; -// TODO [ToDr] - Deprecate when we get rid of old dapps. -extern crate parity_dapps; mod endpoint; mod apps; @@ -95,7 +93,7 @@ use jsonrpc_core::{IoHandler, IoDelegate}; use router::auth::{Authorization, NoAuth, HttpBasicAuth}; use ethcore_rpc::Extendable; -static DAPPS_DOMAIN : &'static str = ".parity"; +use self::apps::{HOME_PAGE, DAPPS_DOMAIN}; /// Indicates sync status pub trait SyncStatus: Send + Sync { @@ -197,6 +195,17 @@ impl Server { Some(allowed) } + /// Returns a list of CORS domains for API endpoint. + fn cors_domains(signer_port: Option) -> Vec { + match signer_port { + Some(port) => vec![ + format!("http://{}{}", HOME_PAGE, DAPPS_DOMAIN), + format!("http://{}", signer_address(port)), + ], + None => vec![], + } + } + fn start_http( addr: &SocketAddr, hosts: Option>, @@ -210,14 +219,16 @@ impl Server { let panic_handler = Arc::new(Mutex::new(None)); let authorization = Arc::new(authorization); let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(apps::urlhint::URLHintContract::new(registrar), sync_status)); - let endpoints = Arc::new(apps::all_endpoints(dapps_path, signer_port)); + let endpoints = Arc::new(apps::all_endpoints(dapps_path, signer_port.clone())); + let cors_domains = Self::cors_domains(signer_port); + let special = Arc::new({ let mut special = HashMap::new(); special.insert(router::SpecialEndpoint::Rpc, rpc::rpc(handler, panic_handler.clone())); special.insert(router::SpecialEndpoint::Utils, apps::utils()); special.insert( router::SpecialEndpoint::Api, - api::RestApi::new(format!("{}", addr), endpoints.clone(), content_fetcher.clone()) + api::RestApi::new(cors_domains, endpoints.clone(), content_fetcher.clone()) ); special }); @@ -226,7 +237,7 @@ impl Server { try!(hyper::Server::http(addr)) .handle(move |ctrl| router::Router::new( ctrl, - apps::main_page(), + signer_port.clone(), content_fetcher.clone(), endpoints.clone(), special.clone(), @@ -290,6 +301,10 @@ pub fn random_filename() -> String { rng.gen_ascii_chars().take(12).collect() } +fn signer_address(port: u16) -> String { + format!("127.0.0.1:{}", port) +} + #[cfg(test)] mod util_tests { use super::Server; @@ -309,4 +324,17 @@ mod util_tests { assert_eq!(address, Some(vec!["localhost".into(), "127.0.0.1".into()])); assert_eq!(some, Some(vec!["ethcore.io".into(), "localhost".into(), "127.0.0.1".into()])); } + + #[test] + fn should_return_cors_domains() { + // given + + // when + let none = Server::cors_domains(None); + let some = Server::cors_domains(Some(18180)); + + // then + assert_eq!(none, Vec::::new()); + assert_eq!(some, vec!["http://home.parity".to_owned(), "http://127.0.0.1:18180".into()]); + } } diff --git a/dapps/src/proxypac.rs b/dapps/src/proxypac.rs index cd225330a..2d7c4e3ce 100644 --- a/dapps/src/proxypac.rs +++ b/dapps/src/proxypac.rs @@ -18,30 +18,45 @@ use endpoint::{Endpoint, Handler, EndpointPath}; use handlers::ContentHandler; -use apps::DAPPS_DOMAIN; +use apps::{HOME_PAGE, DAPPS_DOMAIN}; +use signer_address; -pub struct ProxyPac; +pub struct ProxyPac { + signer_port: Option, +} impl ProxyPac { - pub fn boxed() -> Box { - Box::new(ProxyPac) + pub fn boxed(signer_port: Option) -> Box { + Box::new(ProxyPac { + signer_port: signer_port + }) } } impl Endpoint for ProxyPac { fn to_handler(&self, path: EndpointPath) -> Box { + let signer = self.signer_port + .map(signer_address) + .unwrap_or_else(|| format!("{}:{}", path.host, path.port)); + let content = format!( r#" function FindProxyForURL(url, host) {{ - if (shExpMatch(host, "*{0}")) + if (shExpMatch(host, "{0}{1}")) {{ - return "PROXY {1}:{2}"; + return "PROXY {4}"; + }} + + if (shExpMatch(host, "*{1}")) + {{ + return "PROXY {2}:{3}"; }} return "DIRECT"; }} "#, - DAPPS_DOMAIN, path.host, path.port); + HOME_PAGE, DAPPS_DOMAIN, path.host, path.port, signer); + Box::new(ContentHandler::ok(content, mime!(Application/Javascript))) } } diff --git a/dapps/src/router/host_validation.rs b/dapps/src/router/host_validation.rs index 5be30ef8b..2b7e6c9e4 100644 --- a/dapps/src/router/host_validation.rs +++ b/dapps/src/router/host_validation.rs @@ -15,7 +15,7 @@ // along with Parity. If not, see . -use DAPPS_DOMAIN; +use apps::DAPPS_DOMAIN; use hyper::{server, header, StatusCode}; use hyper::net::HttpStream; diff --git a/dapps/src/router/mod.rs b/dapps/src/router/mod.rs index 2748b5d24..ff60df996 100644 --- a/dapps/src/router/mod.rs +++ b/dapps/src/router/mod.rs @@ -20,13 +20,13 @@ pub mod auth; mod host_validation; -use DAPPS_DOMAIN; +use signer_address; use std::sync::Arc; use std::collections::HashMap; use url::{Url, Host}; use hyper::{self, server, Next, Encoder, Decoder, Control, StatusCode}; use hyper::net::HttpStream; -use apps; +use apps::{self, DAPPS_DOMAIN}; use apps::fetcher::ContentFetcher; use endpoint::{Endpoint, Endpoints, EndpointPath}; use handlers::{Redirection, extract_url, ContentHandler}; @@ -43,7 +43,7 @@ pub enum SpecialEndpoint { pub struct Router { control: Option, - main_page: &'static str, + signer_port: Option, endpoints: Arc, fetch: Arc, special: Arc>>, @@ -61,57 +61,78 @@ impl server::Handler for Router { let endpoint = extract_endpoint(&url); let is_utils = endpoint.1 == SpecialEndpoint::Utils; + trace!(target: "dapps", "Routing request to {:?}. Details: {:?}", url, req); + // Validate Host header if let Some(ref hosts) = self.allowed_hosts { + trace!(target: "dapps", "Validating host headers against: {:?}", hosts); let is_valid = is_utils || host_validation::is_valid(&req, hosts, self.endpoints.keys().cloned().collect()); if !is_valid { + debug!(target: "dapps", "Rejecting invalid host header."); self.handler = host_validation::host_invalid_response(); return self.handler.on_request(req); } } + trace!(target: "dapps", "Checking authorization."); // Check authorization let auth = self.authorization.is_authorized(&req); if let Authorized::No(handler) = auth { + debug!(target: "dapps", "Authorization denied."); self.handler = handler; return self.handler.on_request(req); } let control = self.control.take().expect("on_request is called only once; control is always defined at start; qed"); + debug!(target: "dapps", "Handling endpoint request: {:?}", endpoint); self.handler = match endpoint { // First check special endpoints (ref path, ref endpoint) if self.special.contains_key(endpoint) => { + trace!(target: "dapps", "Resolving to special endpoint."); self.special.get(endpoint) .expect("special known to contain key; qed") .to_async_handler(path.clone().unwrap_or_default(), control) }, // Then delegate to dapp (Some(ref path), _) if self.endpoints.contains_key(&path.app_id) => { + trace!(target: "dapps", "Resolving to local/builtin dapp."); self.endpoints.get(&path.app_id) .expect("special known to contain key; qed") .to_async_handler(path.clone(), control) }, // Try to resolve and fetch the dapp (Some(ref path), _) if self.fetch.contains(&path.app_id) => { + trace!(target: "dapps", "Resolving to fetchable content."); self.fetch.to_async_handler(path.clone(), control) }, // 404 for non-existent content - (Some(ref path), _) if *req.method() == hyper::method::Method::Get => { - let address = apps::redirection_address(path.using_dapps_domains, self.main_page); + (Some(_), _) if *req.method() == hyper::method::Method::Get => { + trace!(target: "dapps", "Resolving to 404."); Box::new(ContentHandler::error( StatusCode::NotFound, "404 Not Found", "Requested content was not found.", - Some(&format!("Go back to the Home Page.", address)) + None, )) }, - // Redirect any GET request to home. + // Redirect any other GET request to signer. _ if *req.method() == hyper::method::Method::Get => { - let address = apps::redirection_address(false, self.main_page); - Redirection::boxed(address.as_str()) + if let Some(port) = self.signer_port { + trace!(target: "dapps", "Redirecting to signer interface."); + Redirection::boxed(&format!("http://{}", signer_address(port))) + } else { + trace!(target: "dapps", "Signer disabled, returning 404."); + Box::new(ContentHandler::error( + StatusCode::NotFound, + "404 Not Found", + "Your homepage is not available when Trusted Signer is disabled.", + Some("You can still access dapps by writing a correct address, though. Re-enabled Signer to get your homepage back."), + )) + } }, // RPC by default _ => { + trace!(target: "dapps", "Resolving to RPC call."); self.special.get(&SpecialEndpoint::Rpc) .expect("RPC endpoint always stored; qed") .to_async_handler(EndpointPath::default(), control) @@ -141,7 +162,7 @@ impl server::Handler for Router { impl Router { pub fn new( control: Control, - main_page: &'static str, + signer_port: Option, content_fetcher: Arc, endpoints: Arc, special: Arc>>, @@ -154,7 +175,7 @@ impl Router { .to_handler(EndpointPath::default()); Router { control: Some(control), - main_page: main_page, + signer_port: signer_port, endpoints: endpoints, fetch: content_fetcher, special: special, diff --git a/dapps/src/rpc.rs b/dapps/src/rpc.rs index 649d283ce..625bfc269 100644 --- a/dapps/src/rpc.rs +++ b/dapps/src/rpc.rs @@ -24,7 +24,7 @@ pub fn rpc(handler: Arc, panic_handler: Arc Box::new(RpcEndpoint { handler: handler, panic_handler: panic_handler, - cors_domain: Some(vec![AccessControlAllowOrigin::Null]), + cors_domain: None, // NOTE [ToDr] We don't need to do any hosts validation here. It's already done in router. allowed_hosts: None, }) diff --git a/dapps/src/tests/api.rs b/dapps/src/tests/api.rs index c0e3bb93b..ea4c08c60 100644 --- a/dapps/src/tests/api.rs +++ b/dapps/src/tests/api.rs @@ -34,7 +34,7 @@ fn should_return_error() { // then assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned()); - assert_eq!(response.headers.get(0).unwrap(), "Content-Type: application/json"); + assert_eq!(response.headers.get(3).unwrap(), "Content-Type: application/json"); assert_eq!(response.body, format!("58\n{}\n0\n\n", r#"{"code":"404","title":"Not Found","detail":"Resource you requested has not been found."}"#)); assert_security_headers(&response.headers); } @@ -57,8 +57,8 @@ fn should_serve_apps() { // then assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); - assert_eq!(response.headers.get(0).unwrap(), "Content-Type: application/json"); - assert!(response.body.contains("Parity Home Screen"), response.body); + assert_eq!(response.headers.get(3).unwrap(), "Content-Type: application/json"); + assert!(response.body.contains("Parity UI"), response.body); assert_security_headers(&response.headers); } @@ -80,7 +80,7 @@ fn should_handle_ping() { // then assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); - assert_eq!(response.headers.get(0).unwrap(), "Content-Type: application/json"); + assert_eq!(response.headers.get(3).unwrap(), "Content-Type: application/json"); assert_eq!(response.body, "0\n\n".to_owned()); assert_security_headers(&response.headers); } @@ -107,3 +107,54 @@ fn should_try_to_resolve_dapp() { assert_security_headers(&response.headers); } +#[test] +fn should_return_signer_port_cors_headers() { + // given + let server = serve(); + + // when + let response = request(server, + "\ + POST /api/ping HTTP/1.1\r\n\ + Host: localhost:8080\r\n\ + Origin: http://127.0.0.1:18180\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); + assert!( + response.headers_raw.contains("Access-Control-Allow-Origin: http://127.0.0.1:18180"), + "CORS header for signer missing: {:?}", + response.headers + ); +} + +#[test] +fn should_return_signer_port_cors_headers_for_home_parity() { + // given + let server = serve(); + + // when + let response = request(server, + "\ + POST /api/ping HTTP/1.1\r\n\ + Host: localhost:8080\r\n\ + Origin: http://home.parity\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); + assert!( + response.headers_raw.contains("Access-Control-Allow-Origin: http://home.parity"), + "CORS header for home.parity missing: {:?}", + response.headers + ); +} diff --git a/dapps/src/tests/authorization.rs b/dapps/src/tests/authorization.rs index 808214a55..86fe4d207 100644 --- a/dapps/src/tests/authorization.rs +++ b/dapps/src/tests/authorization.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -use tests::helpers::{serve_with_auth, request, assert_security_headers}; +use tests::helpers::{serve_with_auth, request, assert_security_headers_for_embed}; #[test] fn should_require_authorization() { @@ -66,7 +66,7 @@ fn should_allow_on_valid_auth() { // when let response = request(server, "\ - GET /home/ HTTP/1.1\r\n\ + GET /ui/ HTTP/1.1\r\n\ Host: 127.0.0.1:8080\r\n\ Connection: close\r\n\ Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l\r\n @@ -76,5 +76,5 @@ fn should_allow_on_valid_auth() { // then assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); - assert_security_headers(&response.headers); + assert_security_headers_for_embed(&response.headers); } diff --git a/dapps/src/tests/helpers.rs b/dapps/src/tests/helpers.rs index c1837d5a2..0069dc31b 100644 --- a/dapps/src/tests/helpers.rs +++ b/dapps/src/tests/helpers.rs @@ -18,6 +18,7 @@ use std::env; use std::str; use std::sync::Arc; use rustc_serialize::hex::FromHex; +use env_logger::LogBuilder; use ServerBuilder; use Server; @@ -27,6 +28,7 @@ use devtools::http_client; const REGISTRAR: &'static str = "8e4e9b13d4b45cb0befc93c3061b1408f67316b2"; const URLHINT: &'static str = "deadbeefcafe0000000000000000000000000000"; +const SIGNER_PORT: u16 = 18180; pub struct FakeRegistrar { pub calls: Arc>>, @@ -58,11 +60,22 @@ impl ContractClient for FakeRegistrar { } } +fn init_logger() { + // Initialize logger + if let Ok(log) = env::var("RUST_LOG") { + let mut builder = LogBuilder::new(); + builder.parse(&log); + builder.init().expect("Logger is initialized only once."); + } +} + pub fn init_server(hosts: Option>) -> (Server, Arc) { + init_logger(); let registrar = Arc::new(FakeRegistrar::new()); let mut dapps_path = env::temp_dir(); dapps_path.push("non-existent-dir-to-prevent-fs-files-from-loading"); - let builder = ServerBuilder::new(dapps_path.to_str().unwrap().into(), registrar.clone()); + let mut builder = ServerBuilder::new(dapps_path.to_str().unwrap().into(), registrar.clone()); + builder.with_signer_port(Some(SIGNER_PORT)); ( builder.start_unsecured_http(&"127.0.0.1:0".parse().unwrap(), hosts).unwrap(), registrar, @@ -70,10 +83,12 @@ pub fn init_server(hosts: Option>) -> (Server, Arc) { } pub fn serve_with_auth(user: &str, pass: &str) -> Server { + init_logger(); let registrar = Arc::new(FakeRegistrar::new()); let mut dapps_path = env::temp_dir(); dapps_path.push("non-existent-dir-to-prevent-fs-files-from-loading"); - let builder = ServerBuilder::new(dapps_path.to_str().unwrap().into(), registrar); + let mut builder = ServerBuilder::new(dapps_path.to_str().unwrap().into(), registrar); + builder.with_signer_port(Some(SIGNER_PORT)); builder.start_basic_auth_http(&"127.0.0.1:0".parse().unwrap(), None, user, pass).unwrap() } @@ -94,5 +109,8 @@ pub fn request(server: Server, request: &str) -> http_client::Response { } pub fn assert_security_headers(headers: &[String]) { - http_client::assert_security_headers_present(headers) + http_client::assert_security_headers_present(headers, None) +} +pub fn assert_security_headers_for_embed(headers: &[String]) { + http_client::assert_security_headers_present(headers, Some(SIGNER_PORT)) } diff --git a/dapps/src/tests/redirection.rs b/dapps/src/tests/redirection.rs index 92fc6ce80..aee1cb964 100644 --- a/dapps/src/tests/redirection.rs +++ b/dapps/src/tests/redirection.rs @@ -33,7 +33,7 @@ fn should_redirect_to_home() { // then assert_eq!(response.status, "HTTP/1.1 302 Found".to_owned()); - assert_eq!(response.headers.get(0).unwrap(), "Location: /home/"); + assert_eq!(response.headers.get(0).unwrap(), "Location: http://127.0.0.1:18180"); } #[test] @@ -53,7 +53,7 @@ fn should_redirect_to_home_when_trailing_slash_is_missing() { // then assert_eq!(response.status, "HTTP/1.1 302 Found".to_owned()); - assert_eq!(response.headers.get(0).unwrap(), "Location: /home/"); + assert_eq!(response.headers.get(0).unwrap(), "Location: http://127.0.0.1:18180"); } #[test] @@ -73,7 +73,6 @@ fn should_display_404_on_invalid_dapp() { // then assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned()); - assert!(response.body.contains("href=\"/home/")); assert_security_headers(&response.headers); } @@ -94,7 +93,6 @@ fn should_display_404_on_invalid_dapp_with_domain() { // then assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned()); - assert!(response.body.contains("href=\"http://home.parity/")); assert_security_headers(&response.headers); } @@ -161,7 +159,7 @@ fn should_serve_proxy_pac() { // then assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); - assert_eq!(response.body, "86\n\nfunction FindProxyForURL(url, host) {\n\tif (shExpMatch(host, \"*.parity\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:8080\";\n\t}\n\n\treturn \"DIRECT\";\n}\n\n0\n\n".to_owned()); + assert_eq!(response.body, "D5\n\nfunction FindProxyForURL(url, host) {\n\tif (shExpMatch(host, \"home.parity\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:18180\";\n\t}\n\n\tif (shExpMatch(host, \"*.parity\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:8080\";\n\t}\n\n\treturn \"DIRECT\";\n}\n\n0\n\n".to_owned()); assert_security_headers(&response.headers); } diff --git a/dapps/src/tests/validation.rs b/dapps/src/tests/validation.rs index c39350cce..ae02a6c2c 100644 --- a/dapps/src/tests/validation.rs +++ b/dapps/src/tests/validation.rs @@ -45,7 +45,7 @@ fn should_allow_valid_host() { // when let response = request(server, "\ - GET /home/ HTTP/1.1\r\n\ + GET /ui/ HTTP/1.1\r\n\ Host: localhost:8080\r\n\ Connection: close\r\n\ \r\n\ @@ -66,7 +66,7 @@ fn should_serve_dapps_domains() { let response = request(server, "\ GET / HTTP/1.1\r\n\ - Host: home.parity\r\n\ + Host: ui.parity\r\n\ Connection: close\r\n\ \r\n\ {} @@ -98,3 +98,30 @@ fn should_allow_parity_utils_even_on_invalid_domain() { assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); } +#[test] +fn should_not_return_cors_headers_for_rpc() { + // given + let server = serve_hosts(Some(vec!["localhost:8080".into()])); + + // when + let response = request(server, + "\ + POST /rpc HTTP/1.1\r\n\ + Host: localhost:8080\r\n\ + Origin: null\r\n\ + Content-Type: application/json\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); + assert!( + !response.headers_raw.contains("Access-Control-Allow-Origin"), + "CORS headers were not expected: {:?}", + response.headers + ); +} + diff --git a/devtools/src/http_client.rs b/devtools/src/http_client.rs index 3f0bd463e..acba2989e 100644 --- a/devtools/src/http_client.rs +++ b/devtools/src/http_client.rs @@ -65,11 +65,18 @@ pub fn request(address: &SocketAddr, request: &str) -> Response { } /// Check if all required security headers are present -pub fn assert_security_headers_present(headers: &[String]) { - assert!( - headers.iter().find(|header| header.as_str() == "X-Frame-Options: SAMEORIGIN").is_some(), - "X-Frame-Options missing: {:?}", headers - ); +pub fn assert_security_headers_present(headers: &[String], port: Option) { + if let Some(port) = port { + assert!( + headers.iter().find(|header| header.as_str() == &format!("X-Frame-Options: ALLOW-FROM http://127.0.0.1:{}", port)).is_some(), + "X-Frame-Options: ALLOW-FROM missing: {:?}", headers + ); + } else { + assert!( + headers.iter().find(|header| header.as_str() == "X-Frame-Options: SAMEORIGIN").is_some(), + "X-Frame-Options: SAMEORIGIN missing: {:?}", headers + ); + } assert!( headers.iter().find(|header| header.as_str() == "X-XSS-Protection: 1; mode=block").is_some(), "X-XSS-Protection missing: {:?}", headers diff --git a/logger/src/lib.rs b/logger/src/lib.rs index 79655d2f6..a79f6fc43 100644 --- a/logger/src/lib.rs +++ b/logger/src/lib.rs @@ -65,11 +65,10 @@ pub fn setup_log(config: &Config) -> Result, String> { builder.filter(Some("rustls"), LogLevelFilter::Warn); builder.filter(None, LogLevelFilter::Info); - if env::var("RUST_LOG").is_ok() { - let lvl = &env::var("RUST_LOG").unwrap(); - levels.push_str(lvl); + if let Ok(lvl) = env::var("RUST_LOG") { + levels.push_str(&lvl); levels.push_str(","); - builder.parse(lvl); + builder.parse(&lvl); } if let Some(ref s) = config.mode { @@ -119,7 +118,7 @@ pub fn setup_log(config: &Config) -> Result, String> { }; builder.format(format); - builder.init().unwrap(); + builder.init().expect("Logger initialized only once."); Ok(logs) } diff --git a/signer/src/tests/mod.rs b/signer/src/tests/mod.rs index e6933382f..9b85f1554 100644 --- a/signer/src/tests/mod.rs +++ b/signer/src/tests/mod.rs @@ -81,7 +81,28 @@ fn should_reject_invalid_host() { // then assert_eq!(response.status, "HTTP/1.1 403 FORBIDDEN".to_owned()); assert!(response.body.contains("URL Blocked")); - http_client::assert_security_headers_present(&response.headers); + http_client::assert_security_headers_present(&response.headers, None); +} + +#[test] +fn should_allow_home_parity_host() { + // given + let server = serve().0; + + // when + let response = request(server, + "\ + GET http://home.parity/ HTTP/1.1\r\n\ + Host: home.parity\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); + http_client::assert_security_headers_present(&response.headers, None); } #[test] @@ -102,7 +123,27 @@ fn should_serve_styles_even_on_disallowed_domain() { // then assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); - http_client::assert_security_headers_present(&response.headers); + http_client::assert_security_headers_present(&response.headers, None); +} + +#[test] +fn should_return_200_ok_for_connect_requests() { + // given + let server = serve().0; + + // when + let response = request(server, + "\ + CONNECT home.parity:8080 HTTP/1.1\r\n\ + Host: home.parity\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); } #[test] @@ -126,7 +167,7 @@ fn should_block_if_authorization_is_incorrect() { // then assert_eq!(response.status, "HTTP/1.1 403 FORBIDDEN".to_owned()); - http_client::assert_security_headers_present(&response.headers); + http_client::assert_security_headers_present(&response.headers, None); } #[test] @@ -205,5 +246,6 @@ fn should_allow_initial_connection_but_only_once() { // then assert_eq!(response1.status, "HTTP/1.1 101 Switching Protocols".to_owned()); assert_eq!(response2.status, "HTTP/1.1 403 FORBIDDEN".to_owned()); - http_client::assert_security_headers_present(&response2.headers); + http_client::assert_security_headers_present(&response2.headers, None); } + diff --git a/signer/src/ws_server/session.rs b/signer/src/ws_server/session.rs index 255f2cd8f..2ff2bc10f 100644 --- a/signer/src/ws_server/session.rs +++ b/signer/src/ws_server/session.rs @@ -63,6 +63,8 @@ mod ui { } } +const HOME_DOMAIN: &'static str = "home.parity"; + fn origin_is_allowed(self_origin: &str, header: Option<&[u8]>) -> bool { match header { None => false, @@ -72,6 +74,8 @@ fn origin_is_allowed(self_origin: &str, header: Option<&[u8]>) -> bool { Some(ref origin) if origin.starts_with("chrome-extension://") => true, Some(ref origin) if origin.starts_with(self_origin) => true, Some(ref origin) if origin.starts_with(&format!("http://{}", self_origin)) => true, + Some(ref origin) if origin.starts_with(HOME_DOMAIN) => true, + Some(ref origin) if origin.starts_with(&format!("http://{}", HOME_DOMAIN)) => true, _ => false } } @@ -134,13 +138,20 @@ pub struct Session { impl ws::Handler for Session { #[cfg_attr(feature="dev", allow(collapsible_if))] fn on_request(&mut self, req: &ws::Request) -> ws::Result<(ws::Response)> { - let origin = req.header("origin").or_else(|| req.header("Origin")).map(|x| &x[..]); - let host = req.header("host").or_else(|| req.header("Host")).map(|x| &x[..]); + trace!(target: "signer", "Handling request: {:?}", req); + + // TODO [ToDr] ws server is not handling proxied requests correctly: + // Trim domain name from resource part: + let resource = req.resource().trim_left_matches(&format!("http://{}", HOME_DOMAIN)); + // Styles file is allowed for error pages to display nicely. - let is_styles_file = req.resource() == "/styles.css"; + let is_styles_file = resource == "/styles.css"; // Check request origin and host header. if !self.skip_origin_validation { + let origin = req.header("origin").or_else(|| req.header("Origin")).map(|x| &x[..]); + let host = req.header("host").or_else(|| req.header("Host")).map(|x| &x[..]); + let is_valid = origin_is_allowed(&self.self_origin, origin) || (origin.is_none() && origin_is_allowed(&self.self_origin, host)); let is_valid = is_styles_file || is_valid; @@ -155,6 +166,14 @@ impl ws::Handler for Session { } } + // PROXY requests when running behind home.parity + if req.method() == "CONNECT" { + let mut res = ws::Response::ok("".into()); + res.headers_mut().push(("Content-Length".into(), b"0".to_vec())); + res.headers_mut().push(("Connection".into(), b"keep-alive".to_vec())); + return Ok(res); + } + // Detect if it's a websocket request // (styles file skips origin validation, so make sure to prevent WS connections on this resource) if req.header("sec-websocket-key").is_some() && !is_styles_file { @@ -173,8 +192,9 @@ impl ws::Handler for Session { }); } + debug!(target: "signer", "Requesting resource: {:?}", resource); // Otherwise try to serve a page. - Ok(self.file_handler.handle(req.resource()) + Ok(self.file_handler.handle(resource) .map_or_else( // return 404 not found || error(ErrorType::NotFound, "Not found", "Requested file was not found.", None), From 8e5c9ff162656e716dcc6472eeac2531e86bf3a7 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sat, 22 Oct 2016 15:22:16 +0200 Subject: [PATCH 02/77] rename State::snapshot to checkpoint to avoid confusion (#2796) --- ethcore/src/executive.rs | 14 +++--- ethcore/src/state/mod.rs | 98 ++++++++++++++++++++-------------------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/ethcore/src/executive.rs b/ethcore/src/executive.rs index 5856086c6..f93424415 100644 --- a/ethcore/src/executive.rs +++ b/ethcore/src/executive.rs @@ -250,7 +250,7 @@ impl<'a> Executive<'a> { vm_tracer: &mut V ) -> evm::Result where T: Tracer, V: VMTracer { // backup used in case of running out of gas - self.state.snapshot(); + self.state.checkpoint(); // at first, transfer value to destination if let ActionValue::Transfer(val) = params.value { @@ -269,7 +269,7 @@ impl<'a> Executive<'a> { let cost = self.engine.cost_of_builtin(¶ms.code_address, data); if cost <= params.gas { self.engine.execute_builtin(¶ms.code_address, data, &mut output); - self.state.discard_snapshot(); + self.state.discard_checkpoint(); // trace only top level calls to builtins to avoid DDoS attacks if self.depth == 0 { @@ -289,7 +289,7 @@ impl<'a> Executive<'a> { Ok(params.gas - cost) } else { // just drain the whole gas - self.state.revert_to_snapshot(); + self.state.revert_to_checkpoint(); tracer.trace_failed_call(trace_info, vec![], evm::Error::OutOfGas.into()); @@ -335,7 +335,7 @@ impl<'a> Executive<'a> { res } else { // otherwise it's just a basic transaction, only do tracing, if necessary. - self.state.discard_snapshot(); + self.state.discard_checkpoint(); tracer.trace_call(trace_info, U256::zero(), trace_output, vec![]); Ok(params.gas) @@ -354,7 +354,7 @@ impl<'a> Executive<'a> { vm_tracer: &mut V ) -> evm::Result where T: Tracer, V: VMTracer { // backup used in case of running out of gas - self.state.snapshot(); + self.state.checkpoint(); // part of substate that may be reverted let mut unconfirmed_substate = Substate::new(); @@ -485,10 +485,10 @@ impl<'a> Executive<'a> { | Err(evm::Error::BadInstruction {.. }) | Err(evm::Error::StackUnderflow {..}) | Err(evm::Error::OutOfStack {..}) => { - self.state.revert_to_snapshot(); + self.state.revert_to_checkpoint(); }, Ok(_) | Err(evm::Error::Internal) => { - self.state.discard_snapshot(); + self.state.discard_checkpoint(); substate.accrue(un_substate); } } diff --git a/ethcore/src/state/mod.rs b/ethcore/src/state/mod.rs index b4e9178f1..02716b8de 100644 --- a/ethcore/src/state/mod.rs +++ b/ethcore/src/state/mod.rs @@ -167,24 +167,24 @@ impl AccountEntry { /// Upon destruction all the local cache data propagated into the global cache. /// Propagated items might be rejected if current state is non-canonical. /// -/// State snapshotting. +/// State checkpointing. /// -/// A new snapshot can be created with `snapshot()`. Snapshots can be +/// A new checkpoint can be created with `checkpoint()`. checkpoints can be /// created in a hierarchy. -/// When a snapshot is active all changes are applied directly into -/// `cache` and the original value is copied into an active snapshot. -/// Reverting a snapshot with `revert_to_snapshot` involves copying -/// original values from the latest snapshot back into `cache`. The code +/// When a checkpoint is active all changes are applied directly into +/// `cache` and the original value is copied into an active checkpoint. +/// Reverting a checkpoint with `revert_to_checkpoint` involves copying +/// original values from the latest checkpoint back into `cache`. The code /// takes care not to overwrite cached storage while doing that. -/// Snapshot can be discateded with `discard_snapshot`. All of the orignal -/// backed-up values are moved into a parent snapshot (if any). +/// checkpoint can be discateded with `discard_checkpoint`. All of the orignal +/// backed-up values are moved into a parent checkpoint (if any). /// pub struct State { db: StateDB, root: H256, cache: RefCell>, // The original account is preserved in - snapshots: RefCell>>>, + checkpoints: RefCell>>>, account_start_nonce: U256, factories: Factories, } @@ -213,7 +213,7 @@ impl State { db: db, root: root, cache: RefCell::new(HashMap::new()), - snapshots: RefCell::new(Vec::new()), + checkpoints: RefCell::new(Vec::new()), account_start_nonce: account_start_nonce, factories: factories, } @@ -229,7 +229,7 @@ impl State { db: db, root: root, cache: RefCell::new(HashMap::new()), - snapshots: RefCell::new(Vec::new()), + checkpoints: RefCell::new(Vec::new()), account_start_nonce: account_start_nonce, factories: factories }; @@ -237,21 +237,21 @@ impl State { Ok(state) } - /// Create a recoverable snaphot of this state. - pub fn snapshot(&mut self) { - self.snapshots.get_mut().push(HashMap::new()); + /// Create a recoverable checkpoint of this state. + pub fn checkpoint(&mut self) { + self.checkpoints.get_mut().push(HashMap::new()); } - /// Merge last snapshot with previous. - pub fn discard_snapshot(&mut self) { - // merge with previous snapshot - let last = self.snapshots.get_mut().pop(); - if let Some(mut snapshot) = last { - if let Some(ref mut prev) = self.snapshots.get_mut().last_mut() { + /// Merge last checkpoint with previous. + pub fn discard_checkpoint(&mut self) { + // merge with previous checkpoint + let last = self.checkpoints.get_mut().pop(); + if let Some(mut checkpoint) = last { + if let Some(ref mut prev) = self.checkpoints.get_mut().last_mut() { if prev.is_empty() { - **prev = snapshot; + **prev = checkpoint; } else { - for (k, v) in snapshot.drain() { + for (k, v) in checkpoint.drain() { prev.entry(k).or_insert(v); } } @@ -259,15 +259,15 @@ impl State { } } - /// Revert to the last snapshot and discard it. - pub fn revert_to_snapshot(&mut self) { - if let Some(mut snapshot) = self.snapshots.get_mut().pop() { - for (k, v) in snapshot.drain() { + /// Revert to the last checkpoint and discard it. + pub fn revert_to_checkpoint(&mut self) { + if let Some(mut checkpoint) = self.checkpoints.get_mut().pop() { + for (k, v) in checkpoint.drain() { match v { Some(v) => { match self.cache.get_mut().entry(k) { Entry::Occupied(mut e) => { - // Merge snapshotted changes back into the main account + // Merge checkpointed changes back into the main account // storage preserving the cache. e.get_mut().overwrite_with(v); }, @@ -293,14 +293,14 @@ impl State { fn insert_cache(&self, address: &Address, account: AccountEntry) { // Dirty account which is not in the cache means this is a new account. - // It goes directly into the snapshot as there's nothing to rever to. + // It goes directly into the checkpoint as there's nothing to rever to. // // In all other cases account is read as clean first, and after that made - // dirty in and added to the snapshot with `note_cache`. + // dirty in and added to the checkpoint with `note_cache`. if account.is_dirty() { - if let Some(ref mut snapshot) = self.snapshots.borrow_mut().last_mut() { - if !snapshot.contains_key(address) { - snapshot.insert(address.clone(), self.cache.borrow_mut().insert(address.clone(), account)); + if let Some(ref mut checkpoint) = self.checkpoints.borrow_mut().last_mut() { + if !checkpoint.contains_key(address) { + checkpoint.insert(address.clone(), self.cache.borrow_mut().insert(address.clone(), account)); return; } } @@ -309,9 +309,9 @@ impl State { } fn note_cache(&self, address: &Address) { - if let Some(ref mut snapshot) = self.snapshots.borrow_mut().last_mut() { - if !snapshot.contains_key(address) { - snapshot.insert(address.clone(), self.cache.borrow().get(address).map(AccountEntry::clone_dirty)); + if let Some(ref mut checkpoint) = self.checkpoints.borrow_mut().last_mut() { + if !checkpoint.contains_key(address) { + checkpoint.insert(address.clone(), self.cache.borrow().get(address).map(AccountEntry::clone_dirty)); } } } @@ -548,7 +548,7 @@ impl State { /// Commits our cached account changes into the trie. pub fn commit(&mut self) -> Result<(), Error> { - assert!(self.snapshots.borrow().is_empty()); + assert!(self.checkpoints.borrow().is_empty()); Self::commit_into(&self.factories, &mut self.db, &mut self.root, &mut *self.cache.borrow_mut()) } @@ -561,7 +561,7 @@ impl State { #[cfg(feature = "json-tests")] /// Populate the state from `accounts`. pub fn populate_from(&mut self, accounts: PodState) { - assert!(self.snapshots.borrow().is_empty()); + assert!(self.checkpoints.borrow().is_empty()); for (add, acc) in accounts.drain().into_iter() { self.cache.borrow_mut().insert(add, AccountEntry::new_dirty(Some(Account::from_pod(acc)))); } @@ -569,7 +569,7 @@ impl State { /// Populate a PodAccount map from this state. pub fn to_pod(&self) -> PodState { - assert!(self.snapshots.borrow().is_empty()); + assert!(self.checkpoints.borrow().is_empty()); // TODO: handle database rather than just the cache. // will need fat db. PodState::from(self.cache.borrow().iter().fold(BTreeMap::new(), |mut m, (add, opt)| { @@ -739,7 +739,7 @@ impl Clone for State { db: self.db.boxed_clone(), root: self.root.clone(), cache: RefCell::new(cache), - snapshots: RefCell::new(Vec::new()), + checkpoints: RefCell::new(Vec::new()), account_start_nonce: self.account_start_nonce.clone(), factories: self.factories.clone(), } @@ -1777,34 +1777,34 @@ fn ensure_cached() { } #[test] -fn snapshot_basic() { +fn checkpoint_basic() { let mut state_result = get_temp_state(); let mut state = state_result.reference_mut(); let a = Address::zero(); - state.snapshot(); + state.checkpoint(); state.add_balance(&a, &U256::from(69u64)); assert_eq!(state.balance(&a), U256::from(69u64)); - state.discard_snapshot(); + state.discard_checkpoint(); assert_eq!(state.balance(&a), U256::from(69u64)); - state.snapshot(); + state.checkpoint(); state.add_balance(&a, &U256::from(1u64)); assert_eq!(state.balance(&a), U256::from(70u64)); - state.revert_to_snapshot(); + state.revert_to_checkpoint(); assert_eq!(state.balance(&a), U256::from(69u64)); } #[test] -fn snapshot_nested() { +fn checkpoint_nested() { let mut state_result = get_temp_state(); let mut state = state_result.reference_mut(); let a = Address::zero(); - state.snapshot(); - state.snapshot(); + state.checkpoint(); + state.checkpoint(); state.add_balance(&a, &U256::from(69u64)); assert_eq!(state.balance(&a), U256::from(69u64)); - state.discard_snapshot(); + state.discard_checkpoint(); assert_eq!(state.balance(&a), U256::from(69u64)); - state.revert_to_snapshot(); + state.revert_to_checkpoint(); assert_eq!(state.balance(&a), U256::from(0)); } From 7bd37e397202722832c8ea99e90312a568e46070 Mon Sep 17 00:00:00 2001 From: Robert Habermeier Date: Sat, 22 Oct 2016 15:22:34 +0200 Subject: [PATCH 03/77] flush DB changes on drop (#2795) --- util/src/kvdb.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/util/src/kvdb.rs b/util/src/kvdb.rs index 7d6ea399c..4c5b7a3d0 100644 --- a/util/src/kvdb.rs +++ b/util/src/kvdb.rs @@ -595,6 +595,13 @@ impl Database { } } +impl Drop for Database { + fn drop(&mut self) { + // write all buffered changes if we can. + let _ = self.flush(); + } +} + #[cfg(test)] mod tests { use hash::*; From 37a2ee98de1ec086df6851565821970250c3a68e Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Sat, 22 Oct 2016 14:24:02 +0100 Subject: [PATCH 04/77] Additional RPCs for password management (#2779) * Add personal_testPassword and stub for personal_changePassword * Add change-password functionality. * Address grumble. * Update tests. * Update build. --- ethcore/src/account_provider.rs | 14 ++++++++++++++ rpc/src/v1/impls/personal.rs | 31 +++++++++++++++++++++++++++++-- rpc/src/v1/traits/personal.rs | 10 ++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/ethcore/src/account_provider.rs b/ethcore/src/account_provider.rs index 5c1398e02..7a4958ddc 100644 --- a/ethcore/src/account_provider.rs +++ b/ethcore/src/account_provider.rs @@ -265,6 +265,20 @@ impl AccountProvider { Ok(()) } + /// Returns `true` if the password for `account` is `password`. `false` if not. + pub fn test_password(&self, account: &Address, password: String) -> Result { + match self.sstore.sign(&account, &password, &Default::default()) { + Ok(_) => Ok(true), + Err(SSError::InvalidPassword) => Ok(false), + Err(e) => Err(Error::SStore(e)), + } + } + + /// Changes the password of `account` from `password` to `new_password`. Fails if incorrect `password` given. + pub fn change_password(&self, account: &Address, password: String, new_password: String) -> Result<(), Error> { + self.sstore.change_password(&account, &password, &new_password).map_err(Error::SStore) + } + /// Helper method used for unlocking accounts. fn unlock_account(&self, account: Address, password: String, unlock: Unlock) -> Result<(), Error> { // verify password by signing dump message diff --git a/rpc/src/v1/impls/personal.rs b/rpc/src/v1/impls/personal.rs index 0830effd5..fde5f10b2 100644 --- a/rpc/src/v1/impls/personal.rs +++ b/rpc/src/v1/impls/personal.rs @@ -119,7 +119,7 @@ impl Personal for PersonalClient where C: MiningBl fn unlock_account(&self, params: Params) -> Result { try!(self.active()); from_params::<(RpcH160, String, Option)>(params).and_then( - |(account, account_pass, duration)|{ + |(account, account_pass, duration)| { let account: Address = account.into(); let store = take_weak!(self.accounts); let r = match (self.allow_perm_unlock, duration) { @@ -132,7 +132,34 @@ impl Personal for PersonalClient where C: MiningBl Ok(_) => Ok(Value::Bool(true)), Err(_) => Ok(Value::Bool(false)), } - }) + } + ) + } + + fn test_password(&self, params: Params) -> Result { + try!(self.active()); + from_params::<(RpcH160, String)>(params).and_then( + |(account, password)| { + let account: Address = account.into(); + take_weak!(self.accounts) + .test_password(&account, password) + .map(|b| Value::Bool(b)) + .map_err(|e| errors::account("Could not fetch account info.", e)) + } + ) + } + + fn change_password(&self, params: Params) -> Result { + try!(self.active()); + from_params::<(RpcH160, String, String)>(params).and_then( + |(account, password, new_password)| { + let account: Address = account.into(); + take_weak!(self.accounts) + .change_password(&account, password, new_password) + .map(|_| Value::Null) + .map_err(|e| errors::account("Could not fetch account info.", e)) + } + ) } fn sign_and_send_transaction(&self, params: Params) -> Result { diff --git a/rpc/src/v1/traits/personal.rs b/rpc/src/v1/traits/personal.rs index 988091958..cf955447f 100644 --- a/rpc/src/v1/traits/personal.rs +++ b/rpc/src/v1/traits/personal.rs @@ -39,6 +39,14 @@ pub trait Personal: Sized + Send + Sync + 'static { /// Unlocks specified account for use (can only be one unlocked account at one moment) fn unlock_account(&self, _: Params) -> Result; + /// Returns true if given `password` would unlock given `account`. + /// Arguments: `account`, `password`. + fn test_password(&self, _: Params) -> Result; + + /// Changes an account's password. + /// Arguments: `account`, `password`, `new_password`. + fn change_password(&self, _: Params) -> Result; + /// Sends transaction and signs it in single call. The account is not unlocked in such case. fn sign_and_send_transaction(&self, _: Params) -> Result; @@ -69,6 +77,8 @@ pub trait Personal: Sized + Send + Sync + 'static { delegate.add_method("personal_newAccountFromPhrase", Personal::new_account_from_phrase); delegate.add_method("personal_newAccountFromWallet", Personal::new_account_from_wallet); delegate.add_method("personal_unlockAccount", Personal::unlock_account); + delegate.add_method("personal_testPassword", Personal::test_password); + delegate.add_method("personal_changePassword", Personal::change_password); delegate.add_method("personal_signAndSendTransaction", Personal::sign_and_send_transaction); delegate.add_method("personal_setAccountName", Personal::set_account_name); delegate.add_method("personal_setAccountMeta", Personal::set_account_meta); From a1266fccb727ccd8105f7e217b290020f0f71899 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Sat, 22 Oct 2016 15:49:39 +0200 Subject: [PATCH 05/77] Make local apps available (Fixes #2771) (#2808) --- js/src/ui/Container/container.css | 11 ++- js/src/ui/Container/container.js | 5 +- js/src/ui/Modal/modal.js | 5 +- js/src/views/Dapps/AddDapps/AddDapps.css | 2 +- js/src/views/Dapps/AddDapps/AddDapps.js | 37 ++++++---- js/src/views/Dapps/AddDapps/index.js | 2 +- js/src/views/Dapps/Summary/summary.js | 2 +- js/src/views/Dapps/available.js | 75 ++++++++++++-------- js/src/views/Dapps/dapps.js | 68 +++++++++--------- js/src/views/Dapps/{visible.js => hidden.js} | 22 ++++-- 10 files changed, 138 insertions(+), 91 deletions(-) rename js/src/views/Dapps/{visible.js => hidden.js} (64%) diff --git a/js/src/ui/Container/container.css b/js/src/ui/Container/container.css index 14a0179ec..ad625aa7c 100644 --- a/js/src/ui/Container/container.css +++ b/js/src/ui/Container/container.css @@ -18,14 +18,23 @@ padding: 0em; } +.compact, .padded { - padding: 1.5em; background: rgba(0, 0, 0, 0.8) !important; border-radius: 0 !important; position: relative; overflow: auto; } +.compact { + padding: 0 1.5em; +} + +.padded { + padding: 1.5em; +} + +.light .compact, .light .padded { background: rgba(0, 0, 0, 0.5) !important; } diff --git a/js/src/ui/Container/container.js b/js/src/ui/Container/container.js index a91f0ab06..143115f45 100644 --- a/js/src/ui/Container/container.js +++ b/js/src/ui/Container/container.js @@ -23,17 +23,18 @@ export default class Container extends Component { static propTypes = { children: PropTypes.node, className: PropTypes.string, + compact: PropTypes.bool, light: PropTypes.bool, style: PropTypes.object } render () { - const { children, className, light, style } = this.props; + const { children, className, compact, light, style } = this.props; const classes = `${styles.container} ${light ? styles.light : ''} ${className}`; return (
- + { children }
diff --git a/js/src/ui/Modal/modal.js b/js/src/ui/Modal/modal.js index fbdf2a6c1..7137c02a4 100644 --- a/js/src/ui/Modal/modal.js +++ b/js/src/ui/Modal/modal.js @@ -38,6 +38,7 @@ class Modal extends Component { busy: PropTypes.bool, children: PropTypes.node, className: PropTypes.string, + compact: PropTypes.bool, current: PropTypes.number, waiting: PropTypes.array, scroll: PropTypes.bool, @@ -51,7 +52,7 @@ class Modal extends Component { render () { const { muiTheme } = this.context; - const { actions, busy, className, current, children, scroll, steps, waiting, title, visible, settings } = this.props; + const { actions, busy, className, current, children, compact, scroll, steps, waiting, title, visible, settings } = this.props; const contentStyle = muiTheme.parity.getBackgroundStyle(null, settings.backgroundSeed); const header = ( - <Container light style={ { transition: 'none' } }> + <Container light compact={ compact } style={ { transition: 'none' } }> { children } </Container> </Dialog> diff --git a/js/src/views/Dapps/AddDapps/AddDapps.css b/js/src/views/Dapps/AddDapps/AddDapps.css index a651e25d3..bff292322 100644 --- a/js/src/views/Dapps/AddDapps/AddDapps.css +++ b/js/src/views/Dapps/AddDapps/AddDapps.css @@ -15,6 +15,6 @@ /* along with Parity. If not, see <http://www.gnu.org/licenses/>. */ -.hash { +.description { margin-top: .5em !important; } diff --git a/js/src/views/Dapps/AddDapps/AddDapps.js b/js/src/views/Dapps/AddDapps/AddDapps.js index 28679a2d5..04cdfeba3 100644 --- a/js/src/views/Dapps/AddDapps/AddDapps.js +++ b/js/src/views/Dapps/AddDapps/AddDapps.js @@ -21,15 +21,15 @@ import Checkbox from 'material-ui/Checkbox'; import { Modal, Button } from '../../../ui'; -import styles from './AddDapps.css'; +import styles from './addDapps.css'; export default class AddDapps extends Component { static propTypes = { available: PropTypes.array.isRequired, - visible: PropTypes.array.isRequired, + hidden: PropTypes.array.isRequired, open: PropTypes.bool.isRequired, - onAdd: PropTypes.func.isRequired, - onRemove: PropTypes.func.isRequired, + onHideApp: PropTypes.func.isRequired, + onShowApp: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired }; @@ -38,13 +38,13 @@ export default class AddDapps extends Component { return ( <Modal - title='Select Distributed Apps to be shown' + compact + title='visible applications' actions={ [ - <Button label={ 'Done' } onClick={ onClose } icon={ <DoneIcon /> } /> + <Button label={ 'Done' } key='done' onClick={ onClose } icon={ <DoneIcon /> } /> ] } visible={ open } - scroll - > + scroll> <List> { available.map(this.renderApp) } </List> @@ -53,20 +53,27 @@ export default class AddDapps extends Component { } renderApp = (app) => { - const { visible, onAdd, onRemove } = this.props; - const isVisible = visible.includes(app.id); + const { hidden, onHideApp, onShowApp } = this.props; + const isHidden = hidden.includes(app.id); + const description = ( + <div className={ styles.description }> + { app.description } + </div> + ); const onCheck = () => { - if (isVisible) onRemove(app.id); - else onAdd(app.id); + if (isHidden) { + onShowApp(app.id); + } else { + onHideApp(app.id); + } }; return ( <ListItem key={ app.id } - leftCheckbox={ <Checkbox checked={ isVisible } onCheck={ onCheck } /> } + leftCheckbox={ <Checkbox checked={ !isHidden } onCheck={ onCheck } /> } primaryText={ app.name } - secondaryText={ <pre className={ styles.hash }><code>{ app.hash }</code></pre> } - /> + secondaryText={ description } /> ); } } diff --git a/js/src/views/Dapps/AddDapps/index.js b/js/src/views/Dapps/AddDapps/index.js index 6014c7315..383c648cf 100644 --- a/js/src/views/Dapps/AddDapps/index.js +++ b/js/src/views/Dapps/AddDapps/index.js @@ -14,4 +14,4 @@ // 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 './AddDapps'; +export default from './addDapps'; diff --git a/js/src/views/Dapps/Summary/summary.js b/js/src/views/Dapps/Summary/summary.js index 64c4fcea3..1989386ea 100644 --- a/js/src/views/Dapps/Summary/summary.js +++ b/js/src/views/Dapps/Summary/summary.js @@ -38,7 +38,7 @@ export default class Summary extends Component { return null; } - const url = `/app/${app.local ? 'local' : 'global'}/${app.id}`; + const url = `/app/${app.builtin ? 'global' : 'local'}/${app.url || app.id}`; const image = app.image ? <img src={ app.image } className={ styles.image } /> : <div className={ styles.image }> </div>; diff --git a/js/src/views/Dapps/available.js b/js/src/views/Dapps/available.js index 24e1c2c8a..3ec7df8f8 100644 --- a/js/src/views/Dapps/available.js +++ b/js/src/views/Dapps/available.js @@ -14,60 +14,77 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. -// TODO remove this hardcoded list of apps once the route works again. - -import { sha3 } from '../../api/util/sha3'; - -const hardcoded = [ +const builtinApps = [ { - id: 'basiccoin', + id: '0xf9f2d620c2e08f83e45555247146c62185e4ab7cf82a4b9002a265a0d020348f', + url: 'basiccoin', name: 'Token Deployment', description: 'Deploy new basic tokens that you are able to send around', - author: 'Ethcore <admin@ethcore.io>', - version: '1.0.0' + author: 'Parity Team <admin@ethcore.io>', + version: '1.0.0', + builtin: true }, { - id: 'gavcoin', + id: '0xd798a48831b4ccdbc71de206a1d6a4aa73546c7b6f59c22a47452af414dc64d6', + url: 'gavcoin', name: 'GAVcoin', description: 'Manage your GAVcoins, the hottest new property in crypto', - author: 'Ethcore <admin@ethcore.io>', - version: '1.0.0' + author: 'Parity Team <admin@ethcore.io>', + version: '1.0.0', + builtin: true }, { - id: 'registry', + id: '0xd1adaede68d344519025e2ff574650cd99d3830fe6d274c7a7843cdc00e17938', + url: 'registry', name: 'Registry', description: 'A global registry of addresses on the network', - author: 'Ethcore <admin@ethcore.io>', - version: '1.0.0' + author: 'Parity Team <admin@ethcore.io>', + version: '1.0.0', + builtin: true }, { - id: 'tokenreg', + id: '0x0a8048117e51e964628d0f2d26342b3cd915248b59bcce2721e1d05f5cfa2208', + url: 'tokenreg', name: 'Token Registry', description: 'A registry of transactable tokens on the network', - author: 'Ethcore <admin@ethcore.io>', - version: '1.0.0' + author: 'Parity Team <admin@ethcore.io>', + version: '1.0.0', + builtin: true }, { - id: 'signaturereg', + id: '0xf49089046f53f5d2e5f3513c1c32f5ff57d986e46309a42d2b249070e4e72c46', + url: 'signaturereg', name: 'Method Registry', description: 'A registry of method signatures for lookups on transactions', - author: 'Ethcore <admin@ethcore.io>', - version: '1.0.0' + author: 'Parity Team <admin@ethcore.io>', + version: '1.0.0', + builtin: true }, { - id: 'githubhint', + id: '0x058740ee9a5a3fb9f1cfa10752baec87e09cc45cd7027fd54708271aca300c75', + url: 'githubhint', name: 'GitHub Hint', description: 'A mapping of GitHub URLs to hashes for use in contracts as references', - author: 'Ethcore <admin@ethcore.io>', - version: '1.0.0' + author: 'Parity Team <admin@ethcore.io>', + version: '1.0.0', + builtin: true } ]; export default function () { - // return fetch('//127.0.0.1:8080/api/apps') - // .then((res) => res.ok ? res.json() : []) - return Promise.resolve(hardcoded) // TODO - .then((apps) => apps.map((app) => { - return Object.assign({}, app, { hash: sha3(app.id) }); - })); + return fetch('http://127.0.0.1:8080/api/apps') + .then((response) => { + return response.ok + ? response.json() + : []; + }) + .catch((error) => { + console.warn('app list', error); + return []; + }) + .then((localApps) => { + return builtinApps + .concat(localApps) + .sort((a, b) => a.name.localeCompare(b.name)); + }); } diff --git a/js/src/views/Dapps/dapps.js b/js/src/views/Dapps/dapps.js index d53d1ad00..e1b2121f7 100644 --- a/js/src/views/Dapps/dapps.js +++ b/js/src/views/Dapps/dapps.js @@ -23,7 +23,7 @@ import FlatButton from 'material-ui/FlatButton'; import EyeIcon from 'material-ui/svg-icons/image/remove-red-eye'; import fetchAvailable from './available'; -import { read as readVisible, write as writeVisible } from './visible'; +import { readHiddenApps, writeHiddenApps } from './hidden'; import AddDapps from './AddDapps'; import Summary from './Summary'; @@ -37,15 +37,17 @@ export default class Dapps extends Component { state = { available: [], - visible: [], + hidden: [], modalOpen: false } componentDidMount () { fetchAvailable() .then((available) => { - this.setState({ available }); - this.setState({ visible: readVisible() }); + this.setState({ + available, + hidden: readHiddenApps() + }); this.loadImages(); }) .catch((err) => { @@ -54,24 +56,24 @@ export default class Dapps extends Component { } render () { - const { available, visible, modalOpen } = this.state; - const apps = available.filter((app) => visible.includes(app.id)); + const { available, hidden, modalOpen } = this.state; + const apps = available.filter((app) => !hidden.includes(app.id)); return ( <div> <AddDapps available={ available } - visible={ visible } + hidden={ hidden } open={ modalOpen } - onAdd={ this.onAdd } - onRemove={ this.onRemove } + onHideApp={ this.onHideApp } + onShowApp={ this.onShowApp } onClose={ this.closeModal } /> <Actionbar className={ styles.toolbar } title='Decentralized Applications' buttons={ [ - <FlatButton label='edit' icon={ <EyeIcon /> } onClick={ this.openModal } /> + <FlatButton label='edit' key='edit' icon={ <EyeIcon /> } onClick={ this.openModal } /> ] } /> <Page> @@ -97,38 +99,40 @@ export default class Dapps extends Component { const { available } = this.state; const { dappReg } = Contracts.get(); - return Promise.all(available.map((app) => dappReg.getImage(app.hash))) - .then((images) => { - this.setState({ - available: images - .map(hashToImageUrl) - .map((image, i) => Object.assign({}, available[i], { image })) + return Promise + .all(available.map((app) => dappReg.getImage(app.id))) + .then((images) => { + this.setState({ + available: images + .map(hashToImageUrl) + .map((image, i) => Object.assign({}, available[i], { image })) + }); + }) + .catch((error) => { + console.warn('loadImages', error); }); - }) - .catch((err) => { - console.error('error loading dapp images', err); - }); } - onAdd = (id) => { - const oldVisible = this.state.visible; - if (oldVisible.includes(id)) return; - const newVisible = oldVisible.concat(id); - this.setState({ visible: newVisible }); - writeVisible(newVisible); + onHideApp = (id) => { + const { hidden } = this.state; + const newHidden = hidden.concat(id); + + this.setState({ hidden: newHidden }); + writeHiddenApps(newHidden); } - onRemove = (id) => { - const oldVisible = this.state.visible; - if (!oldVisible.includes(id)) return; - const newVisible = oldVisible.filter((_id) => _id !== id); - this.setState({ visible: newVisible }); - writeVisible(newVisible); + onShowApp = (id) => { + const { hidden } = this.state; + const newHidden = hidden.filter((_id) => _id !== id); + + this.setState({ hidden: newHidden }); + writeHiddenApps(newHidden); } openModal = () => { this.setState({ modalOpen: true }); }; + closeModal = () => { this.setState({ modalOpen: false }); }; diff --git a/js/src/views/Dapps/visible.js b/js/src/views/Dapps/hidden.js similarity index 64% rename from js/src/views/Dapps/visible.js rename to js/src/views/Dapps/hidden.js index cffbc789e..fa5458bac 100644 --- a/js/src/views/Dapps/visible.js +++ b/js/src/views/Dapps/hidden.js @@ -14,14 +14,22 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. -const defaultDapps = ['gavcoin', 'basiccoin', 'tokenreg']; +const defaultHidden = []; -export function read () { - const stored = localStorage.getItem('visible-dapps'); - if (stored) return JSON.parse(stored); - return defaultDapps; +export function readHiddenApps () { + const stored = localStorage.getItem('hiddenApps'); + + if (stored) { + try { + return JSON.parse(stored); + } catch (error) { + console.warn('readHiddenApps', error); + } + } + + return defaultHidden; } -export function write (visible) { - localStorage.setItem('visible-dapps', JSON.stringify(visible)); +export function writeHiddenApps (hidden) { + localStorage.setItem('hiddenApps', JSON.stringify(hidden)); } From cf9ad991db5b03b4cc4c0cb1ef27b71577b6ab0e Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Sat, 22 Oct 2016 18:02:48 +0200 Subject: [PATCH 06/77] USG, GBP, Euro & Yuan updates (#2818) --- .../images/contracts/coin-euro-64x64.png | Bin 3834 -> 6453 bytes js/assets/images/contracts/coin-euro.png | Bin 0 -> 25152 bytes .../images/contracts/coin-pound-64x64.png | Bin 3524 -> 6554 bytes js/assets/images/contracts/coin-pound.png | Bin 0 -> 24313 bytes .../images/contracts/coin-us-dollar-64x64.png | Bin 4129 -> 6084 bytes js/assets/images/contracts/coin-us-dollar.png | Bin 0 -> 24315 bytes .../images/contracts/coin-yuan-64x64.png | Bin 3846 -> 6421 bytes js/assets/images/contracts/coin-yuan.png | Bin 0 -> 24461 bytes 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 js/assets/images/contracts/coin-euro.png create mode 100644 js/assets/images/contracts/coin-pound.png create mode 100644 js/assets/images/contracts/coin-us-dollar.png create mode 100644 js/assets/images/contracts/coin-yuan.png diff --git a/js/assets/images/contracts/coin-euro-64x64.png b/js/assets/images/contracts/coin-euro-64x64.png index 199f2b53e9a525dd03a5b2b0174ba65b661ad96d..0af66131ba09c4dc6281b03849cbb0717c53a982 100644 GIT binary patch literal 6453 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 P00000NkvXXu0mjf#!6Wo literal 3834 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 diff --git a/js/assets/images/contracts/coin-euro.png b/js/assets/images/contracts/coin-euro.png new file mode 100644 index 0000000000000000000000000000000000000000..d4a5b081ecb236c87dd54cd90e82c41f1285f42e GIT binary patch literal 25152 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 literal 0 HcmV?d00001 diff --git a/js/assets/images/contracts/coin-pound-64x64.png b/js/assets/images/contracts/coin-pound-64x64.png index 442ec50168af2c1633524fec31457e37784e897a..645a9d674f919dfa673f6f6648d950e072d4c09b 100644 GIT binary patch literal 6554 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 M07*qoM6N<$f@1A}XaE2J delta 3523 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 diff --git a/js/assets/images/contracts/coin-pound.png b/js/assets/images/contracts/coin-pound.png new file mode 100644 index 0000000000000000000000000000000000000000..1158f237740b82f96759bd341e179261c902256f GIT binary patch literal 24313 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 literal 0 HcmV?d00001 diff --git a/js/assets/images/contracts/coin-us-dollar-64x64.png b/js/assets/images/contracts/coin-us-dollar-64x64.png index 0ac1249124d3803c847268076b435fc039492417..44255e1f9ae2ea4c0221c290d2dc822993c9b53d 100644 GIT binary patch literal 6084 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 KMNUMnLSTXhL;JY^ literal 4129 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 diff --git a/js/assets/images/contracts/coin-us-dollar.png b/js/assets/images/contracts/coin-us-dollar.png new file mode 100644 index 0000000000000000000000000000000000000000..918a86ac4590659147132a48b46fac7ba5624bdf GIT binary patch literal 24315 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 literal 0 HcmV?d00001 diff --git a/js/assets/images/contracts/coin-yuan-64x64.png b/js/assets/images/contracts/coin-yuan-64x64.png index f485991a77f5de3c974f1e3de853771772a88afa..ec16a144e22f63e8a376583279d5fc763d071ca9 100644 GIT binary patch literal 6421 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 literal 3846 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 IM6N<$f*A@(*#H0l diff --git a/js/assets/images/contracts/coin-yuan.png b/js/assets/images/contracts/coin-yuan.png new file mode 100644 index 0000000000000000000000000000000000000000..1e7b7282afc63ee0ab453b27379024ab78c466b0 GIT binary patch literal 24461 zcmV)HK)t_-P)<h;3K|Lk000e1NJLTq005-`005*21^@s6_IK`o00006VoOIv00000 z008+zyMF)x010qNS#tmYYo`DJYo`Ii2_XUi000McNliru;0hKH0UY#hZ@vHkAOJ~3 zK~#9!?7eBQWLI_X`CEJMb57<RYSM@XBuXGATObBA7=i644A}U&Uq9pszjpJ&-5vO2 zf9QA((bK0M>=*8Kck~bq(b)J!*v||e9-~15gJz)t(5w<wrK(b?rdxNunR(9Hd#(Pk zPiEed#2_l6N}wH)ac^baoA(U=v-Vp5^<QfVzW}S=IPwt{cT0ca@&iBa?>wPqgJjgH zVXdwh*08nNusIwsiY>{MrfF!}M2wNL@DxQsaGuh6`enu3e2-pPart~h2!S#<$^r`E z{s13)xyUWNw4Sr%rAq)$G-gLPVE@tzhgQ~kdS!#7D;u2N*raYEZS0|{L`@`B5{4n* z9VTFAh=DT@tosUL`SW-5z&n_)N_H>KbJ31DF1v6SS6{M+%lFK4(E=<wn1^ew6glt` zD1Xk^(#E0BoLIQ>CXZ0TXBBQdwPCvtJ#&%+Czm*~y2<0mmN?ZKryJO!r1d>adf-Z& zFY(r*9rz5FzZXCRaEJ(?6Ch?#Dv-agU4^C=VuRO~5Nkqen0JYtK5_Z(IWC#6xcZV^ zTzmN?yyCJwT+oLCFo1T6Liv-cdl!A|B~bpHtXs|OsiW}tQ^(nV;sggzFLP{Tiw$)& zu0(qCMDrx^1m8ovM^({82e<~rBO<5(E~9b+RI>>Hk*U?p7g(wN0we{2;2nX4i;+Sb zf+iSkQd%UWn#x*s_Iq4?`K7$}Ro8Ik74!V+?g2a9+)rdh54EF@TvhZw@e(M{gY`gd z_UJRK>^ppzZ$9c++Sp*F9x|wU)MY_a_Mq3tg%VYe6!9tHO~E3h2%?BgK_?<8Zt^#j z{C^p^)9o6-b(Rt!7IRqYET?mLk^FBp#Ebw0Cj<;)OKe)KZ4nd^;oXZ%yzbgpaox3h zdFADO-s0uQx^G9ew)S2*KY!pQOL-30J~MmlBz*JHr@8CE(>%4d#agTM%S%v!qAW4* zsa0t-VPv0BRrB^Mf)fJ_CmC$PjsHKdF+edlwUEwjyQ2D><1$MViz?zLz!vctqzD=l zs!DKB_<$D&8i_F?Yim@c!|N6oEjr$P-7EQxU*5~BcfxB#e(ns*&&3Hn5Y4{z@KOHh zq3`qf=@BQj$7Zoe>hDB)ePn9|G3X)ql#n(NFuWI%gW3!Y4)p=`9!-WSSW?V{GT1gi z%ETf{=TnYBJprLG_67l{T$<QVGb5Q{CWz#hs^Ss61F=L(F}na_IOk}}%Mgp04G3uy zKU!i}+T!{vF5rgO?&Uq#y__9D1=lbbu-!T~M^5i8ue{*EOQ8H2t*@LgyKCPOzVYZ2 z9NubKP7duaV&wu!9Z>~UD0+#M65ha@;0=N}z|o|LI7f0!B5QCsH+@A-+YI6Kl6jG- z#S~?`jWjm}rEBUAfF7Kn8MT<WiKSIbm?c~XMiD~*Go_6c0Kp5vgVPq;AtBbxr6HFu z6x{H-SMlCAUd5{xS-D1J&r6o_XC1gaV|L2}Pjc%Mhd8j+GT5<;Q8h=Z%51fohQ1j+ zOSmSn+AnC$un<sRqNae)FUv;GV>tk^gwX;CwTM|l!@|VUjjiMqb8g~c0*o=6ahKmK zb99tl1rtmhCN5J;COL50z6)CfwaM@A(oIN)P@z`P;89;f@Hnd}+acxJNv`P`?|uDV z-go^gdDkUDUIOJ$X{{|E-Me#7CcST8Hv7zd2l>ilhdG>j)N{MR7obY1kwPQBGgnCh zNdksoe(<7pPa5YDz*`rHq1mzvm~$Y469`R1*j(X?((}(=|7w2Y`pek63*P)9^E=O* zgFSKb=-x{%e*TQh1GU*#?|7K6-1`WJYR_Qj%h)Ow8MqP`3M6G}C0NW+uQ^&5P&`Qj zp8DqiluDt7vp9|Z9kkAYcuXS|%I-qw4c1Agp5dxpi@g1f*YTluUde?8RKxa~E9c4s zFM;wWTu+((@d^0ooeysE$=h#blpI4!M#VfM=`k{oVg&DSTH{H0i<vvb5v8DY0mD-! z`?&#Su<34~;z;5!=SV1`iJ%R0(y+rRzS&~5zRKP`m+<~~zJdSf4SRUqi@<80w+Hx{ z<H<gG%RM~u^eWHnxd>GQDG&)b=kW-pElyiRS{yB6*_TV=iBh5zm^jM#;${~bpCkT4 z6NM_?6fu{BEODp{4BLjXS5Q`-cDMm)K)(_S4{ocX7%uZm7nc0tKYJ@5x@u8g0_6)> z501<}bNf?#?w;>)x<8NY+{IG02V&~TW<fOJ+6Jt}yM(v|7$$<@zyeAE=7E5tebIx` zo8eUnmO<$>gAyFkgrYykD7K8+A-*R#ue5Q1h!KXcr`M<2++tz0&cA%u8~MF=U(T*B z*M4&S=-!KWTz=psP;OfXHa73Gh55Y?9IbbM>PvU?$jJu1@M2c9W=Ktx-AGkr>o^+W zEYT|iBF3n#!8>#eNCKjQ*MQf60Bsn)s6m-TKr$!=#oIW0GzO+er9d%~06r0kAPMop zqK!z6Cuome5^U`R*InA<!|#6szk0>oc}F3gb5I@}rO%8ichle8{S=?O{XtI44%l%C zYfViOrRo*LB%p@3Ofn0KDzv5~?@<XP=P*Lv=8_{Qcmt;}tX|xp#5^k6AyFq;Bd6I? z7QUh$wRm43-ch$LzI6D~Q;!DB=@#lrlJ@Y@V=g${+6gW%H+kP1uHZj;_p5osdE{%J z(*xNbb@{ZH-GBPmw{y>tbymwAu<HUg+YzQh&sS&@@j*co(n-W12Q%sl)P;$d0vyg1 zZ-z6au!xv4Dqh@yj#Hm~WtT`!vF)i$bI?NNN>D+};k_qmOH!pMJux<&ph++%SX7Dx zi(<?TPV?qV=lL%__*UM(`}_drIXQ@f%Nw8ZyB9uj>yg!)|IfewI>&W4tHmycp`vvL z5_9$?1;h%XuO}Ks=$w2USf;#<E}tyg!L>n1h%|tqR4-~!7O@~XMV-<*!`#$bVwuqQ z>hLOfb<g?zp|9|?c!qddFo#m%w4g{zzg}i<Z^%b}>n*%z@2<~=t(A{mxv=NJFBB*b znBDi+cR%^&&)o4aC&F$v%N^9d%)S$JmTtSF@5js>V%c*P6`Z*VK*e0PAQCZYQP*NF z0tp{?yr@B0#R3&2Iwd(HZU-gcHG@*jA%HVEBjq^?eY~|4T2okNDUx^+1xBDJiE^;S zuEBBs@O^LRrnkPFvb8_Ds&t?Dxd-LpCf>MG_?!Q~uRp+-9yrWK{}MK&Pb|V@OA_@s z3!U%OqAn6y%>SPFQ+XTr241DpFJ?=rVyKIgtqzp3o&UK2r2(k}X1-q9fpEKQ&Q0AL zAuHrza=r4`yc>Z|R8n)Hq!?~eZY=Y=Z+$iY>5Z@AGJkHA@IO;fe#gxIr_X(p+YhhN zEMCS+?Wp<-Xw2F{%;l|koHuqk2OZ)9CfS!#dA=LsCc;`3hl-*C$$^Pd#_;0azEj>Q zY%KbaX<mH%HD43g@w9G|L_lD^tw|KLu0RPml%Or8)i{e;7~*@V34IbQ9p*Q#y_o;< zLvQ0^fh&&;-&?(W?q^m{<bPIw=$?VufA`6+a?h#6aL*O2nt*d?ij?LvePa%Fin`b- zxCLjPUNpQ}M~CFE!nouh|2sckf*}YFvFwkE6=w);U$g<ZSf^jI$re36TN4A3Z1sdG zJtlw&i%mpDXuQLMOr&+*j6ww`j*%)O5o&MrcPw)J_;DV8;wV?V;uZYTrRBf=nFe-0 ztq(r5-h5>I{pUB$KKjWo^2j>W^OvxmJV+pvC8hItb$R>kHXbCR(xqXn$iAUOoON4# zXBo%-q)n7L+kLJ4nLdRVJs_h`s7sjGvs6fKa;{@QT6f-J+3yx}o$u&C63}M1NQ@+b zR(iBnVXh?f3!=8HwGE^B3wUHH@~5A@m#>_v?MXA+zhTQS>h1f;mhHWAUJl%Ubj3dW zg@;)kwru%=G{1w5QA6l05X{qV)%1ez{F~JIOx@OFndb2fC&5f6i)9Mn?1)bMqx^Ot zi*dVDoO^L+bXp%LZi?3&%wU;NxAC?vQ=n#7M!JXBVG=W-U7XTM7Ql+`S~(Ie&`=bL zZ(B-*KAv{7=GvY6_%|Q^*Sw(zulyPCnLjm4dF8wu_|CC4`}4p2cO0k}*qGZxQ+h`2 zI`jP!sg*c@s$A$q1vz4doI(@Lo`s6i1ZX_e-Vxn2l_=&B+kTD4!BCF6$I<wUf9sxt zE;;Rhm>rjFAzddJ?nJ{TICULO1a$}`2VRvbC8}t6&Aw;yp5#50y{zeM4QC!TN9`)u zwTFZ4h=2Q+pW^$ce>M;IPX+adYrFBm#LfTpr|#sbb_ek+X+pJ%<QzVE`VpKdZD^^B zmY@y2m{r^xMZk&-=1?n@CY&i7VKJw6q_e6CLUMS^n#36LwlCIFrAVQMiqd*$oTEI; z6WCL4u+j_Eg$E5(ZKki9Qo$+GI?5P`t|0kr<#PrrlmA}BHs(P)7e^-r7b@e3TQWc= zyGLGUtCHHSJ=(!4m-I*c_kZ{<-rnc_y&`XU(OAkOTekP<hMWKFQ(t9u)KK(iaF8+t z8Df4&@<Y_uMBkEJ?C6mkjXD6!pz}86?n)cswL!E-QcIOuDs3pW>7-zqe{W2s;4z9U z>X<<+*tjygA{IKAq}y5(-LWd3NTDr0HJ~*?M-*C9X`Q#<j&ns)XZ<5WB-dAFe_JpL zO_Z9gVJ{lJn9ln6N6i2$g`vU!``2IP$xYsTq_Mp(3Q!(t)7}$3_WzH6^>v=y6oy4f z9XDohlywHC8=|hJ^);<;vhculf(7zcg}Q7=EXS!yJV6tMwiFgK7_Fg7ZFd7(f~JnJ z&u)PcP%A*I{5o5o&=nXZsGkI9L4_&?%Gmjd24zxusio2uPj|d2p{_+GVJ>1Fzc?i( zH3v07P$j5Rs8S`Riuvo=t`rijH#UOf@l!+o>~C)2RAB$1LHm*O8kC0z!y8wE+yB3O z?pr*tl~`N2h%~>0mb3TiWYPvDbCl||^PxO+S&6ZanRhowbdK5wMj<QERl*c-gh|mB z9knDKv1}rprOZDIVFs4cgZb<-bdob?fiikrDliI?gzmx0#}%EUm4H#?ty+}SagfnD zqWG@dV|?xoXFgv8bK8Dy7RR4(^CfG^Xm`JiN7ju0`LFI^wI4qI(*Whn_>$D`edOae z-^Xo-m)YF;a)z$Lh6Cn$JBVlLAB$EUG0r5{i|g2Nb&Q#y03^{#1Rt{j#cP@0mkdD5 zZ3OU^11T66g2{7erdh1UL!nAg*Zn=$orlY9T|{xBc+LKD!jqgs&EW-{)?J|EvNWyZ zABDUtgzj^uB4>zRCqG-Jv)sbHV|_FtRC5f}K~-@7u_gYW&)>@dv-@W4U(O7ae|7hh zH+}K`L!=9@VqgL!TscQugSfNXLX-*xoa$uP3MpHIBIRwn(Ck~063KG3+Ip}qA1t|K z8<PwY6*>jCPON3NI65Vv$HCDIh;KULWrFtZ0aM06%IQN)yJ9e%R6PbAXR1J*U=u+~ zmtNhLW>it4htw5o;()VM?y0MwR*GK5aI2vxdTgd$eDks6TzT=6Z+^<`kFOT_*m(oW z{ZW^1J+!p@fBoBA87$<~p`R}>GfFK;Nx+TH?xKv-j-nH4(phIUyX+3c>>CnEnn77~ zv`3X{qRSbi*kwAR&t<GZ#{rTD(=L)#%vr{d(`=E?&eV6WvB#89@l%bi7}AMyK?w$` zo<b9X!<%BJq>lE{j+4zm%%3}sxNSRyYi#d{a27_v6G9)Y6N^>BaBGvI>@ko_`FG!V zjLR>-{G<EKK6>qpILx!U+WQCX@+l{~KmMiLI8|OiUF}4Ci8My24Vs8|GjLm_WgM!S z%P)1QQv^lo5UGRG_?#Q|mH}BsM?X3WYY3JD8e!Va!epD_vS(X1MQ9K^dv4BK4o-AJ z=_twN;K3vzG&ow!HJEGhU4RozyA)|hS&;*oVB?2LLGrbTz9qVZNy1yAOh(xl<>>5D zY3lKKq)k{oWT6)rsj}&Juu@*i|NZ&9I1u5W4L@^EL3w0U@2&e`_n-dNtsK=|Y?eD1 znBoP><fu}>8z5(2-YV8oI>R&)V}lF1PR9pmrKJwapfrYsV-x~SsAzqKx)LGea$6}d ztKf`RuH$i1AU-f`BkDcASCV3rsg=~6{dN=;(}cJzz%|Q!Vhomp2sJPxI!EIQ5*5CS z{2QrIp93%v>AHL<-g4svP|PP<X=tUvNR&Phn#5dm^aq^Lplf^oqwW9x;UG~k!#jt^ z(A46bV<>?^{{jxxCI9=UZ()PpO@~ISA30~Bj8*x^pT6@T_a7Uu9(H9tq|+{v^_^#+ zxEX-5DoWbA!5hq#6=_(rqww@b19k=|sdGliCz3Z>2eHdi<`R|yTuDJeKlSLR9&@Q= zE(Q7)a8W^p`Nc&VGls(v^JNbiHZ1DdTlz|cUfH9K(6r!uFLM&XQ#zrC(N{-7z)3+t zk6!9iXoXM`NF8e$DTq`S=~<*t%N#XjGvI=%WOorLO-X|R0Z*XByXrXrR8>++XLOAF z5qBGt+|Wj)V<sdEXKF8`#fy3PSmM+7?5C}Eo^?e26Z@3+A8T*=+?@}xT3*O7R3w@D zdDcb2NJ1r@wd>l%7Uu%Jq9ipf{iIYYt1OH*k&u`R#>jV6PM4m<3EuK{X9A)jNAX(Z z{4fbelea!`u&~I=MnhOwBt}Quv{Y75X>s=axjHljB0_K#rh=1*Ylc+y26Lkk4x{#h zN=2{r^cqhR$Dj}@PCv(vIJ8p?DHap9E;(wC(A#wpZAfhC2(6|EKW?bCy{G1nWi4He znj_&*D{+?ZuS$&(8ET2S3;2g`KF*t8an-pAbRKEVKK7~GSPi>Li@Ru(W;#WEZUo@B zNy=srzG?|JY;wb@X24rsbq&As%NMb006kYz`<mK2)I3T-D~3AD1T-mc^;#mSe@3~0 ziV+>GM&<8r`!*Jhr41-|?8Xv=GEg@SoNZRhRSauIia8{d#CnUlGT}Ctc-_vD-~Zr^ z1Z!!%U{WyGIC@b~XAFG8k((?e$+>|X=ozIMMN&#~(cG2^ak1b(-17wAK5`75U!XRj zXvzry$5wOP-tfoSzO#-Xx59B&bb)7^T{DTa4fJ|@SZ-SW^2>K|Z!~*r_{oEw7Yxe7 zW;cH7_NV#I(RK9lSFjc9Y?X9+z~)ky1_P(%+541*ks461sI;t|ewyoFei2tUeD><H zx{2qh!8rtfauwxAf5azuKkNU**028PC;stHic9x$e6Ru1!^%E`TF*L!wX#G<36X-N z4kJ;;A-m%mfAH3K@qt%_&t6Tq>1nv;3fO<<@l6T*r{J>*ZW37W@QJJ7jPpB?SXuV$ z{=Xi&lhySRz1O}Le1S-XnN5H=-m<q_%O46!>FR*Cdzr$tTEHozsPuMR#65=};qSiv z7~eOupHxxgUML{%KA|^%`i=*%3ol`1R3rFY$~vYex>wfr<!s7Xr|C9_cpp#~px5K@ z$z$Ak?@6vMx#<V}&a-dW4_jBkNB_l}UdEo`GO~P}xlU;`um<fHXHTB%kir|1BC0LE zfH+!Z?|k5WufJ^NYVvuU3CQQJtKp_=fW5H$`Oa^zXZJsT@otXo-;XyK9bYE2Ik@sZ zH)vJW2{^}A^T*&1a2D_i!E(+$-|egoFiQ|318He@UC7_xvyc0hesaS81%dJe-2AD3 zx|dT`pADBV52Y0N)ZI8qAZ^zDr5xFAPv`xltLJu5GRJ5rjHpQSJNV{<-{Hv_R?faA zz`uO=J30Q?!&Ew?R~b#*B9&(^Xo$K&<ufgXw5V4Kue|F`Z)C5qdsgT0NP>U7<4(%n z9_HBzM`}1>G)-m@#=zX}U;cQYtPu5-3Wc@=t8>&(fm+q|f*;Tp4O^jP$#(KL-}nyS zGqb0*27mlh0p-Tp(>DU#`N%pCKfR3Z*vUvD{kf86FhV*}t-7XDh=c3$u_|XT2#}E{ z4jnEmK)Hia(dWtIOML5l`)8VS{ot>?o>#y8GEN_SlF`;06p_ZxzKviRT7onLX~8S1 zEei_^{My@QC$u>LD_^?jyBu8JU}Po34Wa5?LVTtnZEa0}xjnEkGt!Thf($qdIL-Zx z{J0H{m0%@hS)vRXv};(eq%C*xz5OS+{h_1ST)FqB0?NgmSKRWQ#_TWdxSOYUUc^$d zho-uO;UH3#g1VZ<J0gqtI8QG^r9;ebVa3^Zr)Yad@+){fPL(35zL2fO*Yg*5?BlyL zx~W^Eqwl@055N1S%a}X0#N44Z_MC3HWY|({70hdo$~<Ney`vFFV+r#KBtMMWm2Cuz zR0(=*L9gjEH&{TM9#LVJ)m*ai6o2@pOZn9qK2RI5`!BxyZH~+3Gz%9Zb3@wpAR(N9 z!;dq!beP`eDN5ZW6iVX~HHI(dkXX`-J^HbviXLZKW!F?nQ#E*7CxtDBWn`nMsAUeX z1!c;8jkfB9FD{_adHQXnOk1Sh9Djb-QSQXwv^qSxcjll3_}ZP1va(s@oQF0dDN;C( zSUMviHBNPn#S_>JGCHPsU*dePYvtUM1eOOaw|)1>Om6CY@A8`ge)B!==E@6q5=U#S zoIc55Skqq!)a`(%G5cjv5$EVteYDZ(W$ey3b;610l;}10kPIGiiqjz%_e$ROnpe%# z%75{e+c>?phOazrtaDrKq9S4VqF{4%m933Uya}X~^#ab*G$U}9MJxgd%Wbw(%41gY zz@RfSUeED3z0bU7m+nkrBylWnZgBHmLw5CFSo5qO3o*ByHv8HGk28=8TnPwH4X0fv zG^RmLR^yCwvkvs^H6K~PT4MA_sRlnn3s`9jzIyLte0PS$iodqU^S*byk!Ix?EREPW zeVWbD5>iA&42=mVj(`wj#E%;#bRvmy&&cSY35h1uq);)44P{XL#%bPn-RrrgVr5px zyl(_w`1-fmYK@|Bd4Pasip*@>%+hGq*4aG0LJ^G~K-1#8+T`ez)+?z1@orKNRE&9F zp3lH^k~*>o#zJ@Q-l0{=2*+*r-_IQ<*KOvY{D%jh=D+}$-%W~!6HmY}&0{{qa_0&p z3mH_#I&jA<G+^H4{Ht0n$`?OEgAnI0<niUiw;nk$Q>*j4H(bwU3&xy_(6)?D9VW#A zMF8)F!g4K(wGrpS1d!QOcnrx|B_D}CQu_df$EN{T^pxMZ{%ZCzy8!14w>`xElMSx7 zNShi$a5yPxqk_+p6<3ydM$gI$_1YSYMg)tLh0Cd0iPSz3y<k4~{|w0yqTg1q_gtMo z=bUEsl6SNMEuN8cFgMRi-SGMQpW>j|XJ!V<+mD-l{*i;M&0WaI&taOIM~Ozfm&wPB zt3+Td!p#Lteg<hehes7O<uM~FN^OaDZU^h-ZoYKSqdYo8Q2xftJimRz8>v@L;L8%~ zbw($iq1{+#-g$g7L=$BwP_<53Bwd-Xd)8%5L=zU~O5DZ?-hcgTdH3Axj`e`C^0{x^ z$0+O}g^D)TU6rHHg<wJQ1huvWmj@3tr%p0jUZF5WQr-cw0xOX$brUiKr{G)bM9Je8 zIWl$CCN7)cy+=)G)QH|QNXE!HQh%PWK6r%3mVYpP_kuzBwfmmnsk&e|x0_KCocCB; zgGRhB$ZC7n)fmHA4KpTIX8}r6m$@*Pg#e;LtIEiE2Gt_pUrl`ddnadVEkAVQ+gMBk zWUxsg4mLI!96v$5w#J+Y{ca$W$W&^yy<<myAR%;vDB8^xE-T;z*Uz@J<#S(uX!m2s z*0KH`YIS-1iW<>?5I~zLX|9IHAS!Wfg~9R?+Kebfk&urQ({6eOcnP>pl$pu$XYG12 z*0*+POI6Hyq^L+Pu$3T%ibHk9E#G;3Hrw}CPMF=c?`hKFg*3iLauCow$Avt#OI@e5 zv0NQ^=3yH$gYFcn4(Amou@mEZ#0451G4!6T>H@xa&psZRA#Zu(rHlOLuUtp9u}r^? zRL;X-$mYqD#KBrFL`w-Fi%)Vng(8e=v;!dp%H(lA5;m53-*vC$hW_l1@v$2I<{xjP z>F=WPKF^Z$6(-qAdI7Y}Z4^VmG-6Gp3Law0TlxAbS`YCWajp|-c5WFW;IcdNd~TR@ zvnU;OWB&4_3KfSGJ(78%1k$dHx#RJJeDl<g=Ue_zP~QH;A@*;zSSU$qcmZoixutXI z8N`SydtEI{whr!D!!nrxD3dDQgQLb`f>coW5=bb<nwC7VqTI26bEa14rr&rcFLM#U z73n3RU-h6FvU>a|T5qvfRiq|IvUx&Si;(4HdAsw36j-P?xV(f9{>rOodVF8L>md$r zv}mtS+qR&fDwr>!5Y!q96uie`9%3cVXqyp*&qEA!`808T4L7POW9-`EWDYZmQE$_p zj#cv0iE6n!p38+9h~zCkj#`9(Ng%4g+#LH?8t&S6{Ol~{{mJaMC-$?}pT}a0C*k4< z@3XizsX|dvYpxg+bGfwI&K!1>#kprmo_kPhCIwy&hc!yT5T~T7&vG%(r*D5?X1pc9 z+h2X*HNX0*E6`Cx={&>X7WjlKBCAWss5jP`4<(WmXOZBcjYBMSGj7!5DX5m7;f8Ck z;uQ=0$;=||2%r7xoou-|YD<WV1eXU$n*!E=HTaak48|!%XSg7FUS!*V8hUz#xVj31 zAyrcPK1c1NSYw5BJ|^4p<u=U*X$Gd(X&1cUip)~DfHTJ^4$=8NeB+U$+&lQu3XmTN z%G)11&QlvT?ffD^$E5}-E2PCpPC7o$cfH2PeIl8(3t>i<QoGrP)6ZohODoif60w~- zdHCcO_dYc<riCkD|9^bLTbZi@O>8LUIt$cn;;hQhyej|zAOJ~3K~!P=*fX>nn=FJ1 zX<NKC^yUI>s?p$ZK~RfaTN&@Y?lmtcAOEA)SMPp^M~|%_b2~vaH;Of#w7SXf<^JeX z0w+zZ-FPSSsz@^Q)G~2(ol-&C7AHp0D`>Qtj04Om$y|1)Iu-toD?!J=bmZD0H|e_E zl6pIMa?NqaldC_OrQBy`|Mb26EVZCzNvZLSM7&K#42SL`^b7#X8GP2+gVJ2nQ7*+9 z);BAY6z3x&XDmyfFWmJwPtMR<{_}V4<u$K*IrV6r*4muC34*pGNF%mRoupY`r7vLG z60HSi7!4vVR&VgGmtV~Fm%EugEl(!nufB8(wy=j`%Hk0rb$ZsY?YfWq24&i2>>6fZ zj@v0JUb8igYwHY7pTfnKfRWk;T-I&cxYpdeiN(~h%4qyDnE+I_zfX&mbDWALUw`-j zkNpU}-1CC+$z^!>#5%IDi<A-}P3KonILPkg#qz<9n<&Ba(IRKLeUrLuNzsi%9+$=D z;L1xRHe1YHa2fX>Jk1>kSDEGd-S@toiXl!S#0XwMBURyHxW)R(V`#mJ(>#HeAP^gR zHex}xc>ndUohdK*rQ5#4_m?)YxgC>ME|X~y+W@J!Tu=Ti3>dbpAr_#Cei5LJG|S73 z*4Ln^DM<9huoThQqMB=s@{lvvaq`Bq2~4pw9|~_us|Jg^dGy!{`%dW#x0K&__&7`2 zN6JN{ZM)NST{F(yeq_u+X2-ZeT}$$@>o_aZHYGCvXP$!KJ3va;=xH*d6NwF2LPZ#r zyZPJOAD*di`I~RLh+n?yB6=3HgWDtu?`Q@al%<0>WOeBzaWteS1x|%h8+uzyyyNN% z__bHfZX(FO8vgF)dl(gqY_%gOvL!UKQa$sH)?FM&>xwDa-O)FQbfpbl^EU6O>l*3y z+xGM+n)OxYfHFpc15XZOMaLRU%Z>fb&QP0<1EJ|FN9DtMfSaq>u!373Jov&u`LLPY z{`f(L)lOP<EHE9u8KodmVib4{-B^%Z_F%5XBuzWWcFLhyfHK$ojDhG-D<-KtlPn6D z^PXlfg2fBC_sAA^AD&%@u-5_~y5ViGx`MYnsnd(YVv{Q#C8DDt8>d&OH-^lsW49@b zo2U4_cl^?9BkpfL!jmV4)L}6@aAeoUC^KPB!dru=%bhn|cvcohCZ)dPhdjylGG+rc z+O;*dPMyRJ2Lv3!3r-9pWlg4lxjcb)ob%4z;H>MTnP15n-t_>bjzj1#a?fLjxv%-b zTljN9d3b}T)(5ox9i(XVjSi3<=hRK|auZO-T<aon(wRbK+8kf>>_xuP_46@_U2qjh zB1E4XT$NH#M5S=X2w^mLF<-uG9}mqC3?F#QFY%g7FT;<N(p6|Q%3ej&48U4KA=p+; zyR?R_51AVbdB@e4^UjxrYi5?c8L{%&FaHxw*oE)!%#E(PATUvNmNNU7Ilu|viV013 zhQ;hgm}qV?g6jng1C6-CL*39UEz_*5)7C?rCcNvqaBNeoXLn8}JD84}w|1RMRC1Gt zq7b4EAq0-EM;<=ZaKxU!N!A3EOB+W%0`TCzCuu^N%TvWe({$~MJIY^jfHS25a<{K6 z@ZCP+Wp;k08$0h4pEY!i=DT)yYPe_wfhH+#;UeyT><EXRS!2MFnb0uTF2MWW{T{4t zk(7D4;6Pl;TN#jRevQqOOW4L1AAHxlex#cGg{*Jgarf@S$4=vWyQoJElmSu~1WxYF zcy{C1D!Man_XL>Rvoj)S)8zNON9#OyCj`%MeT`-~AjWt`jn9<3w5b=nZOa}5(?n^a z5rV@@q*f*QK93zZ!j&QiP7IzaYm@cik&n~~hI3aO_`Y!Szxm|DJhf@0A~2K@u?M~> zIG3w+`LfHTIG?WNiN_@1B;;-vv7t{(8Mj!JmfbzafBTUe_~$b^jtA0d{?&i=XPk_$ zWL<WW*h0dHUK5$OKEo)CdJxK*>-IRl{onlOv&(U}to;7}@PF)na4n*Xm(rvHnwsD< zC5G$rndXt?C%u=<wq-MZ(iM%J7hgiyxxmN=+M>$Mx2!7*ZlSciy(epk0aBr~uIqJ} zO6D{m(Q=Q@w8>kFEk60-_wf4ixzcwh=5D!Y;mS`K>^=MptgNkP6WpbmQatxf?A`OF zx3*DEF~*6X6U9*Hi3sb10rwxC-Eq9hU%+p_=LVX=8omq=0)-bs5E>NU?^6{eadm|c z|BK(A={5L=uid%(#L6nJC~4Z<Gbw~z5GJ;b_8DIlndYa@c7?KJZRrGSE2|XF=a5s4 zq6>7ygL)%&QGM29j-LvbQ<Ytf`50HL>zf>Sdj0wD_u^b9z!Z4$@JZH&L!1wo8yBj( za}G*OCL>AC0A%+MBjqVv-sM)J;;5al)<$l<?~$1v%J04RMt1ffHiJw*w-IqhO=PGM z*Nk|}RWIYcuf2435qATh|JL2Cr+`&+9hc+qBH5KNp=+Z7DwAl~M9<zzjyGq6xW2() zWrZRtl_jihaF(~|=#&_YRJcx$nZ-Dviz!k{lTwTjN;d1nqX(b<;YO&2j~{sEIO=;H zLE3SFQl4{6P5^lZcR7_#xey?D)C<u;>m4KU)J4S;&m8AJ9GqR2=WPp~_x<Yi=;jGZ z?;+((N#i5<glw(x;rG6qS!TKa;jV-1Uml?IyQ$Sf(F5_cb&JR69`WYRpx{h&W*N1p ziO}z7q43fY!<7|$8z}{xC@t}M^q794$E4eBlO-I6cLi-x@#xW$JZ1LBKM<6s25{iy z67+hsIyJR+9?aTw&jP4SI<M&@H*S`Q7|DAYC)D1fy?HjG@wc}>#A7o&o`3aQ@8*Je zaLs@!^oR)N6_FvYx#A*z^9@(c^!)$*&9}227O2A<8pk~iZL)PcbKA6>Y<qgozGi(K z2*rAsKi;4{b(&^%l}Zdx%&i`~Hiij3Lys9^mlOA%XnE_7QPE<7eW%trwEEoC>*RhP zJaLL+8=IuiqqUB;7n!y}IrsK*s_=f68P(Kcn%m_mLUe)X0x1-vs?Qz!kMZEK*&WG^ zyWs<Gy_U3k67WQ+2;R_*WBmRH-o-0tC{O#;E#KymXO?MuyBKKzUv=boq$t9++Zd*d zhit#WvjHWV1;K@Pxy8)#66+^VKx`<TAvv`M+UYUP;VE$x1nuaf&aX^K(Q1Jc4eUFy z{DY0qzN5z(SU{^@H(?+VLdi^9+WzFp@8{jpvuY@VF^8Mh(G(7Kfm$6F`fT-f^X0oA z<cS$t{SUq8O<cKHkVX-q#HEH;Uoyx0-uCj@`I0UC_1A7A^>-3OA0!7?xm#+^s!Yyz z%+X99UT`AYhGw13N=RAeNItfC?ok^_%S#MaPNVfOw=Hxz%afBvnOLVQol#Y!12jPN zC0peJ-#@kb{GdFdw)fEKHBvc;g#tCBZre^(eeU;fl{3>=XL6WOh@+G|ASz3#1Sch$ z98e;=F5>pb4)EQl>zN(X?_PZ|Z-32|6m3muCFQ8*x8C_iUN^(I@b7PZh=-O2gmRvk zG9OE9p%aEQvF_%7Z_CtdyFPi6+boQNVcOz-mb{IITl6YVS(Vu7CF0r!GR(5KU^Yob zMvRdqg5=H|&bbNkK8&#CdOW>0e2!n~05%i*51w2lRekC)qFo@y2<|+HzTphX)tOE7 zK{TN$=5$yWsasT>D^L|0S1>3R`TRZK<MA0D(}#Zj9n7U63mVyJ@Z0Zt>&!m;kruxA z?eDQ%ETB5x84N6D!rUSRMV+9&>pE;UH6rHnQ2IE7ovu@%o8`abf)Havsz-p3mQIn@ zHefKKjESD}s4BHaq~|fs#QXTZIp=7dBbD<UIJV4MI@7Oo0LRv0b!4=7RO2++F60u- z^Jv*5x}5r#!ZwlVWQ4GryasJVcI@WfgQxh;(b@5?*YADVXW#sai_nwD`JEfy!<9RC z&MeITrw5+o+xwoP-E|>Y9;uRN{&a-2XqRX1aw@W|pR|lyZabXLqOE<W2+9Js=jt&P z(#a*-l{Jc_xD;`s6qTb+H4Z}!l7VU@5pluO5?I<Aabk7!+`o2cX_e&$)P)X|f=Nes zT0BqeB$Mvu_N-uMs^7Dzo^RXBoG|niCt~1B-+qXLaQ`fh>gqXe`pvhzhThf*KKRbJ z&rF{7{Skcn%io|W=P)UA`LZRP=C*($X<UDCW{>l2x>lR`;<IQ5(fLr%(PJ{zV<sc6 zt}s|y!o`NdDJkaB*i)st&ms=Vfiym_)xgtBE6)Yx;nfWWR^mesvDB$STyE}3I_Ho) zQ=z#Hm}k@@>$B25Q;X=mu-P<J7hleu`;PF?Dc(G*WBc_tUdwxa`6^z2h5PKRKJ&|W zJ<Q|JoW?Kg0TWC#mo=MBYoJ_LLd#T%KhYp;udA7b`?cLApwnY+AM2t<zbqgo+T~?N z>#I2hY7LdMu5m8lLf2k9qNycFz@*^mQ_lkBVY3^LEN>894-tpAgjwb%O_-?7&n<aw z&$63xhBC#TC2pOPnO(L)Thw_*QnD<4{_c*4*f*nNyYBKu{<9B#;D@#)d?D-c7C!y; zI~llM)}-pTL7`)x+Ku~W=fO@+1=p2_h^0xkA&GPoCC)y1aYm#$Rs@Z=a%a@d5a;vv z?den0Yb(r)(9_t3C61KF6O3A{t&v=Q$<wD+&QJ`w0o?q|>ITX8NhuH5G>xcbMaV1) zzh=NtBI6wIHrqKC^go}lbgJ#kqJ*ld*cgm(J1^$;Cy(&GrP&?dug|cS`>XdI;*sMk zXtjeN0fr-zqZ3#Q>T=I<x(RCI6l|eTSeoctbGH&9&LmKs)<$i&&eqj}M{<XZ9E<QA zJtoc&=dh@l8HI=7H1qA|SlJpd+NP#*gJ>*mY$Bl|wn}iiO<gQAsoOs5OR_!1XxlG{ zn@~nmk*Q9;W^Je8i|ilc18t~SaV1~A@4lIK)tJS4sDXd~kKbTe&J*T#5$lFI)k#N~ zv4GlSOqS=T&}+_~IMd1GTgF)yna%YdH(5Btsp|BYIB;b_GKJK3ddv~^(kbfYHHOV7 z8#ouTYbu7%!w3Nw)QRn6oolsa(6orp<f!i?^(qc?nU^{r)^o%d&*Ck&bD8RNqNFy- zPiI{Z@kK?Wg7xRQ<9iQtVq@*R0_7d|KF*^D598-|vbi;&6i;Q^`Hvk*KCx6&pISP` z!P&D~ML&zToQ*5Rb1Wv#<wCmg_st0Bgd&s-R#&Ju)-V&iFD7lS)goxFz1kWL8P-$d z7gk3lbyLDzixiGV6Tt<7D6v(XUz~;d>8w`kT2fd*TcZ-CiklQZFd|T^QdA`xN*X+< z;%Mn<q%|SBId-%oDrYpK5mm3i=rO3S;p4ZRqW@bvxfDKoHM8*vf710N@X4>-O4_l2 z3keB^N+5;>s*Y<+UWwjyTemT9L*}SKpy|!C-p+#*IkVCZK)1k!v+kyxo^Ony*?7!Z z2huQo-?>a5r6#w6IkrLBKFR#z4u)l)SneT7WW*3RS8}wyfJ0mP7*|gn-5Z*Q$q3Ey zmYqNQ{NX1<a)^V*Iv&>mRZ1drC~0MxLJTzrX7s2jtfetWbaVK6orT3kHU<Oyz|;4R zFkIo_-A{1z)fe&hYj)mruFvuFfA=j;9e9-ah20EK*YuO8@A^m+DSa+;h>~brAh`l> zP^J+sHJB8nvZ3hjqIC&P4ZZ->liD`t%g^ERA+UaWnZlsFpog^$!F#H*!iGaqN{qVw zd)Gym9|#*;H8CZU3F5k$7;@&ed9L9Z5LEMl<e@@&7HyFlF7$~T-+C=Ot){Y^|L&3J zWseYA-y_<9UNuJ(b3e_1vFNX&UOB-<YymsZ`Ga@K9Q?s={TeLpq_u#=+={O-kMk`? zOT=hhp!Fr-@TtXFq_%=D-t!=b>rH&sCs{<*FcT;O=Q=DfOCUnKxk2kns@=QsPOz9L z#L>1bTU%3>Cv0pDiQVK@oL~ycWKz6-J}ec@C*~n~A>v48xRlr#B7g9n%edTl@0G&M zKj<@_hHK#26Bu57-s}c{@c#F_pr0R)H#uUwcU`#oyZiQW=<sQzD#1BSBeaTCJ?tEF z&8i7kcr-=WSf?op!rUCrpbP;lF&qv#Vz&26kpr%78`L->V=~^xb6w22&wWRyG`;~J zNiLGSqYfn_=LrS$fFB0PF&JlA#C5LM_*||OZeD;tp{*%h7T1ylCl$Cp)`oL`uwrOE z?2MdYOUsN_SEyXxwMrkT<23#bZAw#0Wn7%zbz6{UWk$}K`F9DC+{Gqm7d*I<hKA_l z`C`z|T_D)-(by{97qs}?t2}wUdc>7P>T?XrJl0w?PuV55q?MLUO_V!!<zdq)aiz!s zfKazh-nLy9^IFHPOk`{4!!~YtdaARW)sNya3ebR4{|gVwSC&u|1xbgrDS-qu8Ucs5 z^B^Z@Mp1ZTtDp^p3Ywa&)60~mh_sWDDgdr&#sZA<E8>1u{6?@qX-3ty^dgj+1A%du zuV3VKSotG!Mk1k_XY~}N!v`a#`rIF&2yI>GE^tneHd1&`uW+m{pJq7Nq;7^BGW$pl zt~)YwTxmA$JGlM5bG;IMOg*AP5SOzSDP~!n>MuMfU+(z0BjUW~-a@V=NsUT_g!3c$ z;hoPZc};jpkn)g&!aLSi*EoIh6b5?%LQ(j!FM8gtBt_S9F@ldoGop_;m$HI}msZYj zWD&|(VwT)<#VqGK;DK?6sR&)UhFYSn8?F}lSO%pJov1`RW_sLsbDB>+_mNJ>LyBZP z5+i3Be3wp?mmrzvtdGlVyRK?tj+C24pJObJ)1rDta|AI%MDsv{Z9nhI5GHwhIU~QU zob&wRm?B8slm*6ugy~b~%>BZH@-X~SvZ9-nSb&z@yv#5$Fz0qn_p)iPA<GRUFbe0n zV8^cM{my-|yBo~2b&GV;`g40?m9|;w+-U%`D}4^Cvl7C*sF%WxSb<YTEaXfM4yWgX z#%r4;m#HSyi88)(Meyw2InAoMepO5qIOCT?&x^|1iId#5ww8%1M$VWquD{@*WLrxc zKPe5#V&`$ghI8ypGFCQ?D=U&r8Su7kDg2b=cXPdd_ba;1Vczx>o(JHJK6bMMA<0zK zizG}U%`Z46z3~Y@)>0A?S{x0=5OwDPC1h^0^G-_BWm@M$QWoasr`vZ`Ra0N`JlzKy zpQZiT#pr5>yQZAcEA1~hC<PAW(Fm5yS0oc@(@@o)M@KvDQ#Ce%RaJ4(MHfte;?nt= z-3vmK9O@QmrJ^Ye^^qb)7VC3QxL8ycB{AO)DB4KhFH+Q=J*{WQC{VtL0JQI?;B2Vl z##Rer6WLMqVAvqFBd`<NUFQJ~S1@Wsau;Z#(u<M#Wca2<-G~@Q+X9{7^G>h1yTq7G zH_NF<IS&X-^MMw_d?0F|H6an$l!{~K_}lMn@wKfJEYc9fF~Fgr!Ux0Ztq!!~Iox>` zw?hdEWrE80Xd(=}(D=k$-{H4Tvoj94>4taA>^%OrZ`_A2TmVgEr&D}th+<giffba= z;Y?8?(QDRDiV*2j+K|FVxm62!qOVx)u)Gq($Cuq4buY3th*0$KWyz?GP*mq0l;#le z-Bgj(O%riM%M*}yEiABWzBdJBuPo^IO4w}hRW2CMACfKq`QTHkgGL;<GFRsr;DlkQ zIBLS5e&JRIClBN50aYkS<}mdHN3KDZW=bdFGO64Z(g%gqB&hn-UKqxjUXiFaS9sOV zIbQcG@8I<_xUb)OYLkEa=U-sB<1z-rnjMu<x*?<3l3Wk0muqb-VT@4s|F`$%L6%?F zec$Jtd++b}Ua!+VGuUSU3?Kp`L4sIBQj|oI5<qI9Vk#6V#bv3yl3pZNV%nBml|)%w zWyvX3y!|7k?5N@>Wd{}KkJy#Ogq6s$wZx>$CS^(@L69H_fLNwy>E*rO?=B~Q+~0fM zJu?6S%maHs@K*iU(~aiqx18U(_nh-xAWLPkbh1KY6I$z$L4)3KweLqEnj^O>l%>^b znIgtC$GWkPJxq`Icp<I=u;(YsYIv{a5jr+jMr<x0XaSBqY_2TfE=0fv4#<9k=-L=k zLAHX5qN0@2)fuW3b}ci$=_cCsB_=OCPukl<{Ro$huIm^zE9|;-fS9ibjE$-xlJ|kV zK4NW4kxU`tn-QH9(t?z=9fBKe(zhomW6!SJ<wAds+-AhDfsLtMuMV`U&(S<ms=$6O zfm9-(L}>?>VnWHNDn%P|E>+IOdc-T%v`{8Tzk8dInb|TAVhQ3%DBg^<#<6w)VW+L| zi`TEO;Y*LRg5nTy18_|Sr-vi1E=$Jcf-@tONK+;RnGjr$(KE>tM(e9ICr&`qP~xOo z(rKaODZN3m0ns(PtdPuz6ehVRTco%G)-&DPp`?fuBP?jA(7eaU3nkU#QP)Q@1wS5D zDlpYmBa4wWQ-%g}A)_`$Z3-n~xll@}63${I$#iLt9QS~ZHb7$u>G!~<koT^M;$|>) zK9DJuq#h~8iRFgn0fY^pw~74R%_ol0l!%mmkZPIns*QXNY~Q0|G`Zk%u6jCTut>Iq z33%s7eP(TCoy{9h;oD_Wj1&ZQo>0BmX}IJPBS@tcH?2o<b<|p28ZlBuVk9jn(P$&2 zIHge|I7jOpt#imU5)y=Lgj@(_gBnJ{=Y+Rhjg@dn1*0LdpEy*EcGTd5hnOib(mG|y zDL%yte7F``LRNXg`%WT);SdV9Uw?9rrG!cz=GK!NENdoOq_-LPq4tO>&hls*urAl< zlu91FS?WwT<7vtG$sT2A2MuG!p@lGFyW1j`3BkAceu|jlN_}%|=I=t)=IQ&MX1oN& z$R$!pbbH&3*4Jqk)J^HkC}~38eF4Lx+7V<*%n7drELLA7nUY;ck&K$dm4>MsS2}9W z73i$F+9bOkA<~r$GD4bFoO)8)BjrSKf`*1tjy_W|%;1$$kAzyg=gy3~Z@uZzZ+n0n z*OplInLUiD3<NFMmwr3y=DCaqXYU>(BO1s!z_F}T-hJoI9M2u}2_w+ncAU6#9lP6G z6@o{p*3VWgp!1+uh9sO7g7aj9Nla+t7<)x_&vVi%cQ2@TU0Z2*?|0nAM33RL!RLfZ zUpF%gxEzL$ttYvD(BE;WDN&bjx!PN2QIa^a)?TEjkzLEDzVI~9PqYffrhs<==NvJX zDoJ<LL1{YIIjS9oT1b2AIdx*~@}Rt71#Vaw@#KZ`xYg@PIfEraJ7$s>WH4|wSw=lb zp^C>DIT_x!lrD11n(~Psyq#0jL&=xm(*lp*c7Vq{BUv5%zQ98Yk6y~#d4lT6y+|8m zfcrMr`G-IK6MO|eej0xHY9H^baEHN}1ix?|9{6*A{U34m;vRW)4B9a%MSO8Mgv`+v zJ1J+-rfLcqsQStWo9=Sz<R-V?aPsn?+-P{@)aJ@Zes5<NT*bF3N{l_`nj^=rW#0oC zUpjp0a3;huMsi^_PPmEvQ}XTb0323TyEO1#ia@sLV7zoKA|Nk)4PVK8d^PX>a#MOT ziGtL_O#v%~eAEM-L9L{uOmIRmA&5hL=G2LG?p|%=(#@XRMLv4l^_%G61}nvI3OV(2 z3*Dp5{u7W6p~G9mmol6W8=OHqWVbi!I_>!7U=bk?ov(sO#xcd)^yr_UuR|EIQhS!! zASFVYaQh9%uXqBj8RmxLxZGi}KLA@-t_3zDnn4Rm>qN$=#-*8-Bn{~`&Tg-VRR$AP z6REqMlX~ej0MoWAi|w@9EQ<s*M*QA3cb~fP@)({6<&7I`td6-*GTu2XXVf|DT4*VV zf;dufL;)SFo{MBM?px3`VDPT3vt@Chg<dk(1X)BfW`>GUa&;UFspGn3&s*<2_2QZG z#4_A=^9@k)9GB;nI;%YLcU&NQpdm{j&tOkdr!sGJknfZ5FbwGYLY)<x)p4Z11`tB3 zR9%xoDOHow<;Y#P+`w(ujURr|plsmWowwhH<tm!0%HU6P^mp1?Kc$HYR|eZ)5{@|F z1`GBE56S`$Ijeq&;}r3!Lt=-P30w<kQ?(jKrPRth12J>&U3YTY%cCzElpcQWmYY}W zfwUN2m6G#eU+5!Rt7*bGceFX-OCAEUN9hL>^lt^FiqTl2Ej?|T(&T!%Yhd=x;09-A zgb?aPiozYYUD+;B_j^fOzwq$JF7FM`FzzB}If3~#Hv5Z2bwm|<SFfltfNljXj*tTF zaQW;I6N*6Z3cbJjGilEny{^#vK-7$uL|Y=GA#>(8cu?LB4_NH!Ez&tiD{FLNndv05 zYKzbk^842z^Mm1*;6=<yhu_tbkVfg+!h~J2o3L@b;hwv0eCeQ+X?*;xensB=rf)+f zqApi+C#uN1S{(^FvjO2e*cuNA@O*`gwWu6BRio2HqL4(%PRUM44ovd53LU_;OSmv5 zyFk|^(1vz(gT1`qocz-J^p)!z%N0hcZN)-%6CQl?-JD##aw~8CC}&n~co5*;H=Tj9 zgUS?9L#@SG##zRfGQZR+iY2R2bO_8r@pWc}tfmW5ZR>+k2J<3QB{@g0N*c)b%^N%* zzhr#6H|R<sArKnSK+=}TYoNMzh{vTb<JYUtj-vvT+is8d-v8#g_`uf<?K9UebHlNK z<O?)GDY=UASCRJ`khRz-Ac?Gns5Z=#XFvzgBGm<~Tzya$H#i<3RqD?CP{mbWnFbzt zBL}4lkNN=486{1rDA+{qr_2;kuCC8mhU=v=q+Y&t&$2jz60qb5ec{Z;3iq8p`SSDa zjUyg@&%I}GduQ=vx>6hHN)5(?fjQg0O#w?EwBQTLW%9uDoIOP`K^MT)6|3+IUyP6y z&5D)^-SXlQ<MB6gQ10~n>~ykA>y02GY+{-K03QNLL_t&;(gf!+Y8{fMi_MU)LZCv` zqvFvlxXFa~z3tn1_c1RoJ19?!Jo>%gemfhs196uWnz>zuSOuKOrQ5R1^lN7AnYz`A zEl8s%*LILckYY%wmTHKnqhZ_LxD%Zx;bCo{L+NtHdqpH8IpGCz>W&~7b>EpnsPq&Q zTuE%W!uub5+t)3tE?eKe2JgJ}dVc-sJ*MMLq9~zM0=ckn*ITDX<<KEmE{XSrh#Hs< zmL0ho2{|EqC1qg?;WOvpx#=F33%wg*&Y_kl*-=CZQjy%%5>*IU5%Xk$sF~RBG4dW~ z9(+qC<8ntcInVce+sz9Lpx=0WixgIn;F-q0@`a>Yc{s~B1`nKJPZlcRH(4`yuve}3 zYNcIK={@d~7m6?)9jABanP!J+Aaxy0@Puah==3RvI7!9hq@Zz!`|h}z`))h_@GB0= zyG7*hedY=KohLuTMM;<{5zwMI8DvxJ@E_h6r!LI6y|y4Kg=oN>uXfjFh*OF;m}Z_Y z@E893Uu9|gdHP95ro}mjFS#DA{HS6S$rurj5K5rxep%(?xGMJFcNu((3rgHQ%k66; z{{Aoh7u>S|p3c8`;sSr}=YNT5yISY>QoslLT+N!)62USu=+Bpmw8mK`+tR=WG_n~^ ztjeB9A|WK#Fm+4xZViiutS)0k$Wm&iyy|0R;gz9k%b~<nT+2u!K5g-iH{Z+Y@#PKk zuN#yA@45Rl|MPGEA!pMrX}nyyAtFd<NL^1U5SoTudWz<KBTE>H9UTgvt2aXTxv3Oe zsXj+gu-HQzxM&^gH{Zebxn(97J5moWC6=XXZSM6E9j{dC#Ee>y9abt}L0myftUlq5 zr$bRM1n0QWWOf%s9P7fE$<hY9qjmaHDS5>QBA%iFEfGJU&$DQKp)=RoynM*SN=Z1| zHwH~Ok^~9?8jp3Us`=&~M_@7Ig_o$fMIhJSlM8{IGn9gJTK|ne-s9BqmLGZFJNWsR z(G|VodRrs%SAX+M_PO8s9FulD^m&w>$%Jt;M#Yh0#EA~u%uK(gfN6F8nlT5jvMuCu zs5aKFsy_&&qOfZkWyg&eU4I)T?7(y9$(ERwSe>szOGh=PM3ih=fhJz7Yk~mkDCRI6 z$#m#vDQRdrEv7=QsPRZ3Nwv*1Rg#4oKq};7I1!Qz-ixk4iFl{;%sC4RcR<*y%$-Ct zGgpO50xYN{(n3KQS9q+P!8x32_3bH!I9Dx6oI^vPKl?o2^?h&Q{xy|X@fY^j_rL8n z&WwyE_Jka<+!MwX5Rpozil{hTk$K@`RG#)C`QonldC#+&V;apc)o}(%0uvb#)=!|D z*MV!urRspo%m`>!t!1f&k|{zxi}(Us44oMb4$!@(WEMr`Z*#7JG>CKgQi*WY3Jxe% zi^x*Yfn>$-nvICiAr>(@j6{)C9bwJpf>yp{ywyv|3l_I}3GZ-`O63@QjJ#JGuawl2 zQ;&8#93LtF(fi)@YQuhiy=jd{e(242;C3#s?m=@$+c<iQ^rf<9YEafYm%+^PU_dUS zc^+L=rJYI16`6<sVD{wbV<tJr%JE~g>#Nu(5Q7n=;An7oyftWckQB{=n8)LA#p84R zN^z)pv?w7L+67e!O#w}clm>DovH7g^MkvGg1ws~FQKWbzYkf|K#&|QLst(S!wU}cV zC(gvbiYQ4djrBFjj)%_tRo|(kjB|n$rKCs*j^LGi?yG#@9dF}<H(xJT{nu_6`REVb z`zCI3h1E1+RSSE&=fM@+$dgJrpouQ?Gf%C9>YcR+jOoM|I$ubG#c)(1DrBt`U^Ad? z$(HFPv2@)gVRM}%O7Tj`ij@Es5E_g?F^`q{VcLLVHv-U5@PO8gx}aoJa3pDoG$d&$ zget#MJFr8UIEW<_1sg1s5ljL_L$y^_KU@u_BeISw*jXJ}9GT$~A*&<1*9y9JKnT^W z(tRm-b;N#3ijh%hP%~~B_58%c-@Bk&0Qm0foIG^zJ>+MfVZ@Z65u~R`M1xc}(s?01 zyL)E=Dd+<B@f3Qp#;X1*Q1C;B7(PhzkR6y1voTc&o13_kH^OKQqfjVSx5s(#p|)5C zllH9pIzvOhrQTc%L~SQgI@i!?Ai6;ELh{vL=AblQ8L$Mi4Q6(O*UJwdnXkZS-0*xI zf*WM^%DiuktFL9TZJq;wjQLlan>d(N#g3p#>^deFxB0*WZ{mk;UY3OgCBP3ncmt=_ zmyyX1ORZp*C}nUcmHkH0p)6@qTS$v=ER%?k2$rf%Nzr)&-)E)CiklYN><Aj@qfz`4 ztD86AS5BZKsgw$fn5KbVkw7EndeB7Ho~#L628k5yG3}8BC7R?4oo0$Jm`ha!Lo>Jp zt_O`2?J(_z@2h-E(GIM_3Fn1OnSCu4qgXO(edRA%hob{JC{vJb@xge>&s7w1soQqW zG|m&8Cw3hho5%R@_rGg_hlOF?vI5`zz}vBYkA6DA`6@Y)V^__^uhh+-eYX%By+SCN zkn7pZSv_~vbC}RXVa<(ci$e$cZR}w>h1Hc4EG@T4FrdS%T6rKf<D;p5mAHRt#B}!k zVm{L<#WK4gGR5WL2B`swc9%T|T>bY<5Y?BoFN|6eVhL9gY7uWeO^Jj&#g!@ED$$NI zT}(i}NMpQ?5fv*1?|t?1E)`+soN)jB_w&%V-S&#!#g}yoz9H`3@uTm&>5ISpd&l_f zwxgAXkuy@6Qe2_;V~jD*GRtw7Hg{xErf!Mt(9+n|$(lB?;((%F`i=}lV%O8t?>C99 zAhmvAmkg)w#GXF~=eChaB1{VsGQ|n8-zHlkNT@Yz5g;Ip$VAeDx+$F~NQ>k#E*Wh< z!8L&$8cNfENj+pp!OTPPkTc%g@VYOleus~FCaaU?A7wZkIEy%e_z~nOqddV4_Ri5S zRpI}l&}V!RtTYst>B@xb*yXk`<rm-o79Lz^Sv9Ov&C25^1LxlV&igs0k)@%kHf@KV zS;5gEBr=Z9wL}e=lwm75+Er3!d2@qieI3(U{3L_eN7Oe27l<)d{HPh^%2V>93!^Bt z2$B<4x;p*xia0rxgeq1*4@ccKv8u*W7A4^ZImQe`upY0~3@l|sU0`Jt=y%Wa!{7g2 z9z6NVsS7VV58NTL`JwwxaPM`i_={U;7L1l+iX#$AhqFj@%ARYP_&^cE+qED?S1ZKP z+6v<n8>FVvQE6I498NrHj^G*y0ZO678lY}bwNC8urIM$_8+hm?VWgocegHdQg9fMQ z(8V~~C1ub#BG5}q5eFosOz~w(?Dl9zqv}*Q*&*#-<lZ}O<;Q>IU9ax*dL_SnkBI!S z_uR*3*(JmtAz%`!9*+%~QWSFmljfSql!{~8b)=w-P97s1UxRTA;>guh6*DL^B`ELf z{M;PPf)dt}2F6}-l8JfBlt>~|wFWCzQHC4np!CnR5R|?o)F7+JG*tC8GhFGbluoG9 zihVz2x%DiUJ$~v#AK)(c>Ih!1_!a-yZ6o=y_uPkUzd$1eDZ}IeYKQD4!JtRAe;aMv z(o>kKFg|%4j;+nxE9x3TXsbF|jHtTmZWc2sEh<rFv{bD)Q4lXxq)N(RqpCx_Ah{-# zlvM_(Aa1{qwaCn+$F~i!+he5x*?o>b@;wjm-g|EP^lQ_qf06a0@4lJ0-?WNec$QGo zFo~{rkhh2{UG-5kM`C;pWXg(RN*Ghi^r|eaZ=%Of!f1&^^~LsnD0Z?{nQb2q)eTow zeGg7)HYlYzH;9N{>YaF*gW5G<gn%DJ4XS)10~oFh3UFP@j9N#&_#C&dJO0dv9_Eb5 zgWu4gJZ60SC*S`dr$-&jHbqUTQ({k35~Dn!EfMhavV8Qnx+yWnO6_#YwC}JGSUrB6 z(Xma6I`(3un1KtBGU5W(jySg<Ka@RfD9+b$Tr8wis${wIgLXLsfMqyN&eP@5)8dMZ z7=!?TzBbFzK{}^)Mn;-A#tuL8<3G&1R<4eh{i+6|x2pWg7tDV7gAbhH*M92@^y3?u zx)#HsB@#$PQKF1NM{vZ&PASkdBcdg8%7oC8Q(-R`mNqsBPT2nH)1)E@f|WXfd0}_T zr@y$xm(%Xta$1I>NklFiq0W9j+j4D~bmZW9?0n(;G~(79N|EZ~n)(6n=YU+DXXmWY z8jxxnxtemuBW9@dNErarQb)2N_|hZWU*pF=@SS|@9d}>#v%RVZ_^g@zm;dlL_?_or zx^g{xeb2~cBu5Biwpk*}n8vn__@K@qwx!F}U7<)p$t+1>^6U%r7k02zh3wT$Sk4#G z-E)j|Ww3D>{Ll}K)x5d^GdKjITxmPnWzVlO37bS6SCH-?ja{7{1SD5!lNp}UswStF zOwl8jUMb5^ri?~~n5F{?s{&hEC-{qB<DIvy@pu2)pX4p!>eVZ*3PSQ$5&7pA&7S@r z{~2G}RhE}Gh^CZ*{+dgNTUml5)`Fj5;?*j9Lv1)6snV-qIkI})CP4!`TjwhEqC|Gg zv9!9*IXgZFo;Yg8oE1>O{`W5um}*<}GA3yY=8)kzSg|^PcQP1iR+ZtIo@15HJNimT zNVKhIy1NBU81RxU)^#jf&qmYpSN`}%77~zG9h3m?UlaKUU)Zw0`N*SO2%cSOpk1ao zfvzKWTSsofW}+BXA5q~z@39`<qjw||mN(YHId;x%LFuq|nW;od@`u{UIs~R^b$ZxG z+`q`*Z&raY*d7lCMK+w#211_AiB`kAWe2%WM_cR!5^>t$+ZI0>G2Q8r)U)9Ve(P!e z(#Jo-51w9I*xj>`SNnZ;Zv5gOfB!pacD}~)Ao~{kj;0Y@+`R^>$AcNHW*~Rak5U{v zeMj`l%El)C*anQ2!H>x@!UpPrS;lfCr=FZ6rBsT=KHKj_E@|qy(QICA*G^gu?zvg( zf!p_5$dL0OiIXblr^<BNp<dW@h34Ed{Pc%Ez+ZUJU9#BByrA`YGy7Zr*Jt=A|LzIm ziBs&BDse0pNgQznZh@i^B;iY;NggcFIi>Rjq{C4NeWBG6&N<GX-=ds-re>_z4QMDi z6j(16Jcnu^b^B!gQb}sFLTEq3mIE-eGB2oRnf@r(xSLSghRzCzu<0G{xu^Nj?|3V} z@-rV?_}#OxS38RN;h%iyZG7KdH_*QD1nXsrj|EA3P2BB9W%pI7nVP}e%pco%OoXH` zA=8O6UOk2$yB@TK;_77%w5gfTd<6)b$Fi$e*}ph+4NyMIc!?BAB*_PguQETQ&gS3y z;OV1(|Fmg|IU!CMIb-_VSNN`PyP3cI$A0*=DNGmjK&Ko3C?Nkjo1Ojp|2MyX?ku)) zJyRJIk9@?V!*&T#260gih%1Oz&QS<pp54A@>>JkCjx$Zr<w%LK5~R#()|`zbRbPC+ z8Gfz>++e#`$Q0G#?5JQ>z@b{I%+464;<#ltW<1(nWp9<qN-0>}<}IgB@>l=#Pw=LV z*XE3|@Z0b@g+KojAK;dab;=~+baZWG%GvJAK~{3D%TwRuLcj!)6|@QDWaL~pxp_Uu zmK(+a#Cn|d1R`D%UJ|Wly!BVUO1(4{B&F(THlxPQN^ocUKcf=k<i!sHN7KD92kJqL z&Gi-j^G|%7ci-5&W{2L_?16v%T(O_~wST}@EMe|_L^9K~NAiY+KuiK|JRhqmeML!2 zt{K7tSe+7y?9MA(Imj>9a>>W0^Zzc)M#_5C^J7JrZeN6RTd+IDl}K<<N>8+iH9FwV zDgvNNZ$OhBtvI&BRjTP8(wICb`oI;ksP4C<W^;5O>V05_FNU*%X>CQ#OuR^$21RmJ z=}_<JN?+Y(R|I$QEH{@afBQfEv;4^2*S)4y*lTuRIekp{Yk%rbaVCuDcDKm=HqIAD zOAXl);vq;uVkDF_*C@gqTHN5pU1(D}M6W$KMbW|5PV6uMc23I>7P9q;2+PaMG;7D8 zZO8_(Smnlu+ourD(y<KOa%aSa`lWw;eaI`y<Rt2G!xDu^(n1uYm|!N9SkJXx-oZk| z#;`|6bOCKw&<ihc+zCJbiT~_156IUvDCL5kd;5q-{;LoFQBJKF)^wLfBNs28rvPo+ zl2gZ0V|a@LPq}LRh<SWbMme*T;$?c5VmjAPnkjQ!NleRFjFwRuoH9nM%e3pOu+$Fb z@PhgVnQ_T8NHU~kObSU0y)0hI$1c%_JakW7sR&JxP$~~RyF${c_c`Nfk4FtgB#a$X z>6mmEupp>6Qk-&%p1<)I|19sibA=~%>|?Kev;G^f{_(`@@BB~yl*i6T(#9?9O3QSb zXuPALprv3qq6CW6t;$;(Y?Lz>n$bMq<dQR%9r&QGv`wpB{k>-?;|07Ev}C3iFVI~$ z57S8{DU>P)W0uJ?Eg+pS2^S#Bekn)h<?wzF{C}29PcM+fV@~Ho14`}LQWB=%M=hz_ z!+A#|M&8-w<mLu{>m%>uBX2(~Pwd#oZe5XIdEI8p*_uAF`>_W{ocnKn@`Jp6V~NrE zEta~>T3cOXZ4eYVh%02E>L!KGm;Hd8rM?5#EDme+dO6R05qL!JJsGPixlKb@U&pPk zLeo;RVY!MMH0Ef~>XoiiDXyU`By-GEFkUth{eTo-=X<iN=Sa*ATSx(kn0A;q{Ah{P zIfyMQxj~<Qj{B}_`Fo%EDL(S{(*p2~2*__(rkt%$MYCW0U;Y)p`}8)u?FRnX1{Zes zQ0MA7P#6@+W`Q;UD9T|_V-3=Bd38OAV9nHoXODBWYQnzM542zvO)s?|h!?hr=P$s{ z4j~&JLmeb%^qMPg+yVX7b>0phOYGHXOf=olE7bvVH1tMw&{E0{O6|Q~-?rEbU*=tR z9Ovgg{$srRx}|UUXz3f;nY}*7JO10B`fh&Yo|7D#JcYk-o`w;{mPyi*l*)US2}Sxr zcBCETTFRW9R|8n**TE0&(qd>4h0cs7VL)H3I@2@_&Dts}o5ykE70d;qLAC?anrj7x zZ%ef$Z3V|<px<UFEtC~t6{ju7y^){!JRf}f8UDMU`N^*vkY9f8`CoY5XG(x4CtGK_ z(Z&}a`8SXAtN;8nTwFfQ#jsB2gzTy@s7kETDW$~-_>wNk5;_mU{r1l+JKCW$rb!v} zc&;W>);V<%NK!Q-l|q9uDn@ti9P#``(%v>+3~`X?>vQq$AUF6`X3Bj~#w#k^X6H&> z20C2XMF-AQk`ZRhU@hK^b=iZnkMq<2@%#9%Km6TCI(YZr=s$RBy!kjFUpD)N>rR~f z+5h4H{1jj6CwRBPv^bKDDCSTFAB<9_6<-Jy8IDwri&igdE46||v_icuX*VzG-8HlN zxKdk0RW=Z&7^961a;e*4>N`jg=R8Uw?)7N9v`<Q_4^}T#oI8xie5rQO!JG1uC%2Cb zpG}-#PP@o^2_J-<1@+^~l1zeX1?V=DbF8@?KJoF7^3w|jSG+DV<-zrsnSFi&fA@cT zl+S)Gv#ZC^r4vLEI@=?bi-gw5WmFN|gK=optf~9B6vN4&*J%6m!`XIUw4hg+DKAwM zv>D!P$YRb4tq7BITg0<xtJrCdSnO$hpp*T6=M2Cxn{A88;edV7OgYPvdZk9{T(nfZ zuaB8>1uO$W8)Ba;4OTr}*U<)OttX#*iU;qwp1<-ZKgtIe2tRw>1|`5(tbgbQ!YBXf zzkQN_^8frH&&FlS%5ioyQrbw*geJE`5mbAFHU}RnP6Lw4(678aC>L>bzO>LN$j~RZ zVK`U_0-7S-#f#*vv(QBX-~}d@E^Efu8_Ny=FTVcv{i0j9890Ymt%3?EqTbVnFd*cO z6qS?%)+bh%8f@=b*6kc0efUBC^oQTiTU6xnv*$l`=J>G(-|#^>TYr$vKKo3^uRZ!{ ze)lUckT<Vm#|8GJu-x_Fm7<O$N^%}ikC|Yp;Kbv~tiY+QB|Fq))`h=qg{sO{hHbKR zQ^q0Ch_L(I3#9WGa5*EXkX_sl!dX^45STCLrg+J-r0N_XG6ymb;`IG=3aa>}6)eMw zQ_|K`oLbKO#XtJP{Q2*E%Qa^Le9b+fPi3=z{ICB5|Hr2v<AOiIjx4d}5=CKZg~>qb zvZk$)(5bd)T+#i4=Ri;vxq6YUS+Da-J<!=GQ&k~FS`mW5?%A{SXU`4&)V;Y7*um_0 z=~n*A8z)1{%(*J-jL~?H<wVXI6~TK)%Fwt%+&RmtO!(0EJiy0(^aps?in}IVsjtZ= z^_ZFc*3%vT=wJL>9(#71i&>E1Db0w^3saLS80A}>h8mbf4*?ZSY+&78eV;Px<?W{Z z>{aFgW+fp8ty4&uy$jo<-5uCDJI{`@;4IZL?UlO(+oe#sQiZTWgHp(O7|Y54LUNAP zevj$)^Su2{H}WSw`~$q_?pq$YUF4%&qsw25PwLJgh5UOn`_0ch#jpPEGo0VsW2c+Y zxFxg+Shfm06+aNAuO#@t7%bwfA!EbDa2}WgLR>_MeZ+f@sL{nl(=;rfIL?LOY2prj z-&c(y*q2XyB|-UO+1~aA`cq0!3ght@a%Q?WA#2Yqoa2KZdWiqzk9_B=mQs1cXUf@n zVz2v{jhi#S2|xSmztiy>|K?LXKbhiISLuDAGe81TWU$@SdFy1Z`YVNyuP#&0+KhT2 zTh0P<CZ09ZX3Q9>TK{c@5CWrTzRb?f&is7moSRd2Usg-#vf1~+|2EjTl*-C%gU3py zJAZ+-;~U(6|NZ>Z4}3ch9QW6JFupE=a<-n|d+N-|rJEoBm*;lufB*l!$R~gAX};Qz z$ScPowB*twB%CCQC3*x4Bg)_sftDd*N=2Qgj7JF~XfZ6Oxj#=S85?-GB1XoT>gu5? zT!)6`WvHZK)HxR;7`~pcp0m$>jkLX8TUaUKbFO2_Qfai=AR|Ook_*HB>p;NuQ&bEr zRVxxz5<<+1`3A%joTAem<owq-wUl`O{de)<_q~(vyX*ApCKz8=K{?BsGHxD^@V|ck z1vVc2^ym2P&p*kNdxf-moNWvAzM+Ibwz`dp=87&a4LRpStr(G7rR1CmAyiypDOE!V zP^taCjM2#6fl62}X%*=en%jrrWlc2NreWvYIl8T_D(YGa)^}B*d+tG0=j(YU!R8da z!{rM|5poj50xC`AkBi_ulztbRY;oP#c=$Ws%10miE`I2imDgP`zV3qZO6xbCK4<^( z^H1~1&ppMLx-n;Ei85ZT-i>jJS`XF}!m%oQJajH&sp=HsJT~KCs~gT|GogD|%j;5T z<m_eJs9d_W%B3jy{h#$QFbW<inaTMrIDZ~;tTc$&)dwpHN(FQ{=PHpcCCCZF9{43Z zW5^AG28kK%cL{l$TaJ6)`Ib|B@OvNR{##Ezd|F=GX6{=-`3+dVwQKgT{@{83-~aGs z9y`0scJ{<DLPEe$UKqp!8Xs!*i{1P@r#gK2vkH2kxti>^Z{{@G%bJCHg_7tBU3VFi zrxQj&X(Y3^^(^VaStuQvhb`Gk&DMEn&f`$u;9Q_*Z!n>9gbcd7$0|K<J97iyb<gd5 z=;80+-eviw_$<FEg7V3&^Jh+N9(x?%v10c7PxbufXTQv+zW5AZy%?Di*gC$7mBDvf zJvkYv>`S6J=P;`-w`Q4Q5lE;LRG}-!4$YnCXT}$KGxY$pP7K^ke<)39ozX~Ud+S-c zt>+ObkP^<Sf7{%Gg@JyF(w9WgLYQ9U##VUf{x|cX_q>B2yyMt6O(1^LWy&k9Pw$%j z!B;xI@Z{I{XOI0MyOYRXE@U?*`6Z$slUze5z8ZLnL$nIqIWkuIO3^?_)(k<okJPE^ zD{=t)S^q~5&@yJCqLTal>O5yc<54T@Uc5j$f3B*pS;m)4$PpJ4F2_pZ%Erd#3irMF zPTqI_TX@Hv*Yl1Q`KJBczNv%q;QGB{_V|+*`OITq=5tRy$75%AcrH42+!A|kOc}4z zo1>rv4OQ_u7s#^=HW`9Z58ycm`{P5KdAWp5S%XrYRgrq{$tI*Mj7F8>vAy*?>G`ig zoZz}`#@$7Zx58aFU&noSpW)2u(>!?3X*QSinr?W1(>Ko-xz1jA@?*z!KeIe;_Ngbg zdEob-d4VrJdx596r~Khl&$8Vc7pIA29uq&WzC|1os?(R%vZ&6zP38!94s6@|j=t3# z6bhDlT+YNvq!D379O>dSG$nD<xMB5{6*j^$r#5coo?CC=t+(IIZ6}u=xjnowg76KQ zDPL-R#q6OAiBG<;lX&v^EuMVtEKhG;<b?~{Y)^W+KJj$FL7x*b56-cJ^<1_=b!{-r z)FC^{%*|CDK^;DHtc+UL#-6ptabnzZ>+yBYT))B1>&u)vF+O)kkWId|z7>=&wZ3S! zHO*}F1-@_*`X2T=W718T^c~YK()T?vr|PaHO4BrqM-Ahqr40>B?TDs<jWt+qVI{x{ zFe*H9yL)41!EXiS)n?J%_z(!Em;Jo&_oUgGQzDPgvg}ZWp8>d4D%<Z#voiqm-;0xn k08WSTqu+X0zyA9F0byae3JVz>KL7v#07*qoM6N<$f;`qo=l}o! literal 0 HcmV?d00001 From bf827a758f78392ea5d9e3477c71cf978daf5f51 Mon Sep 17 00:00:00 2001 From: Jannis Redmann <mail@jannisr.de> Date: Sat, 22 Oct 2016 19:25:30 +0200 Subject: [PATCH 07/77] fix Signer UI (#2750) * fix Signer UI * resolve conflicts --- .../RequestFinishedWeb3/RequestFinishedWeb3.js | 1 - .../Signer/components/SignRequest/SignRequest.css | 2 +- .../Signer/components/SignRequest/SignRequest.js | 13 ++++++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/js/src/views/Signer/components/RequestFinishedWeb3/RequestFinishedWeb3.js b/js/src/views/Signer/components/RequestFinishedWeb3/RequestFinishedWeb3.js index 168d306d3..7b30d3e93 100644 --- a/js/src/views/Signer/components/RequestFinishedWeb3/RequestFinishedWeb3.js +++ b/js/src/views/Signer/components/RequestFinishedWeb3/RequestFinishedWeb3.js @@ -46,7 +46,6 @@ export default class RequestFinishedWeb3 extends Component { id={ id } address={ sign.address } hash={ sign.hash } - result={ result } msg={ msg } status={ status } error={ error } diff --git a/js/src/views/Signer/components/SignRequest/SignRequest.css b/js/src/views/Signer/components/SignRequest/SignRequest.css index f1981986d..03ae73009 100644 --- a/js/src/views/Signer/components/SignRequest/SignRequest.css +++ b/js/src/views/Signer/components/SignRequest/SignRequest.css @@ -15,6 +15,7 @@ /* along with Parity. If not, see <http://www.gnu.org/licenses/>. */ .container { + position: relative; padding: 25px 0 15px; } @@ -61,7 +62,6 @@ } .actions { - width: 180px; display: inline-block; min-height: 120px; } diff --git a/js/src/views/Signer/components/SignRequest/SignRequest.js b/js/src/views/Signer/components/SignRequest/SignRequest.js index 2c026edf9..25b3dd77d 100644 --- a/js/src/views/Signer/components/SignRequest/SignRequest.js +++ b/js/src/views/Signer/components/SignRequest/SignRequest.js @@ -25,10 +25,13 @@ import styles from './SignRequest.css'; const nullable = (type) => React.PropTypes.oneOfType([ React.PropTypes.oneOf([ null ]), type ]); export default class SignRequest extends Component { + static contextTypes = { + api: PropTypes.object + } // TODO [todr] re-use proptypes? static propTypes = { - id: PropTypes.string.isRequired, + id: PropTypes.object.isRequired, address: PropTypes.string.isRequired, hash: PropTypes.string.isRequired, isFinished: PropTypes.bool.isRequired, @@ -75,7 +78,10 @@ export default class SignRequest extends Component { } renderDetails () { - const { address, balance, chain, hash } = this.props; + const { address, hash } = this.props; + const { balance, chain } = this.state; + + if (!balance || !chain) return (<div />); return ( <div className={ styles.signDetails }> @@ -95,7 +101,8 @@ export default class SignRequest extends Component { if (isFinished) { if (status === 'confirmed') { - const { chain, hash } = this.props; + const { hash } = this.props; + const { chain } = this.state; return ( <div className={ styles.actions }> From 20591e882e64f3190cfd24f377cea66e07c3d68c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= <tomusdrw@users.noreply.github.com> Date: Sat, 22 Oct 2016 20:06:30 +0200 Subject: [PATCH 08/77] Return old-ish content even when syncing (#2757) --- dapps/src/apps/fetcher.rs | 39 +++++++++++++++++++++----------------- dapps/src/tests/fetch.rs | 30 ++++++++++++++++++++++++++++- dapps/src/tests/helpers.rs | 13 +++++++++---- 3 files changed, 60 insertions(+), 22 deletions(-) diff --git a/dapps/src/apps/fetcher.rs b/dapps/src/apps/fetcher.rs index 9d66276ea..210c6b180 100644 --- a/dapps/src/apps/fetcher.rs +++ b/dapps/src/apps/fetcher.rs @@ -69,6 +69,15 @@ impl<R: URLHint> ContentFetcher<R> { } } + fn still_syncing() -> Box<Handler> { + Box::new(ContentHandler::error( + StatusCode::ServiceUnavailable, + "Sync In Progress", + "Your node is still syncing. We cannot resolve any content before it's fully synced.", + Some("<a href=\"javascript:window.location.reload()\">Refresh</a>") + )) + } + #[cfg(test)] fn set_status(&self, content_id: &str, status: ContentStatus) { self.cache.lock().insert(content_id.to_owned(), status); @@ -84,12 +93,10 @@ impl<R: URLHint> ContentFetcher<R> { } // fallback to resolver if let Ok(content_id) = content_id.from_hex() { - // if app_id is valid, but we are syncing always return true. - if self.sync.is_major_importing() { - return true; - } // else try to resolve the app_id - self.resolver.resolve(content_id).is_some() + let has_content = self.resolver.resolve(content_id).is_some(); + // if there is content or we are syncing return true + has_content || self.sync.is_major_importing() } else { false } @@ -99,28 +106,19 @@ impl<R: URLHint> ContentFetcher<R> { let mut cache = self.cache.lock(); let content_id = path.app_id.clone(); - if self.sync.is_major_importing() { - return Box::new(ContentHandler::error( - StatusCode::ServiceUnavailable, - "Sync In Progress", - "Your node is still syncing. We cannot resolve any content before it's fully synced.", - Some("<a href=\"javascript:window.location.reload()\">Refresh</a>") - )); - } - let (new_status, handler) = { let status = cache.get(&content_id); match status { - // Just server dapp + // Just serve the content Some(&mut ContentStatus::Ready(ref endpoint)) => { (None, endpoint.to_async_handler(path, control)) }, - // App is already being fetched + // Content is already being fetched Some(&mut ContentStatus::Fetching(ref fetch_control)) => { trace!(target: "dapps", "Content fetching in progress. Waiting..."); (None, fetch_control.to_handler(control)) }, - // We need to start fetching app + // We need to start fetching the content None => { trace!(target: "dapps", "Content unavailable. Fetching... {:?}", content_id); let content_hex = content_id.from_hex().expect("to_handler is called only when `contains` returns true."); @@ -141,6 +139,10 @@ impl<R: URLHint> ContentFetcher<R> { }; match content { + // Don't serve dapps if we are still syncing (but serve content) + Some(URLHintResult::Dapp(_)) if self.sync.is_major_importing() => { + (None, Self::still_syncing()) + }, Some(URLHintResult::Dapp(dapp)) => { let (handler, fetch_control) = ContentFetcherHandler::new( dapp.url(), @@ -170,6 +172,9 @@ impl<R: URLHint> ContentFetcher<R> { (Some(ContentStatus::Fetching(fetch_control)), Box::new(handler) as Box<Handler>) }, + None if self.sync.is_major_importing() => { + (None, Self::still_syncing()) + }, None => { // This may happen when sync status changes in between // `contains` and `to_handler` diff --git a/dapps/src/tests/fetch.rs b/dapps/src/tests/fetch.rs index 9f02e3385..1cabca5cb 100644 --- a/dapps/src/tests/fetch.rs +++ b/dapps/src/tests/fetch.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. -use tests::helpers::{serve_with_registrar, request, assert_security_headers}; +use tests::helpers::{serve_with_registrar, serve_with_registrar_and_sync, request, assert_security_headers}; #[test] fn should_resolve_dapp() { @@ -37,3 +37,31 @@ fn should_resolve_dapp() { assert_security_headers(&response.headers); } +#[test] +fn should_return_503_when_syncing_but_should_make_the_calls() { + // given + let (server, registrar) = serve_with_registrar_and_sync(); + { + let mut responses = registrar.responses.lock(); + let res1 = responses.get(0).unwrap().clone(); + let res2 = responses.get(1).unwrap().clone(); + // Registrar will be called twice - fill up the responses. + responses.push(res1); + responses.push(res2); + } + + // when + let response = request(server, + "\ + GET / HTTP/1.1\r\n\ + Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.parity\r\n\ + Connection: close\r\n\ + \r\n\ + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 503 Service Unavailable".to_owned()); + assert_eq!(registrar.calls.lock().len(), 4); + assert_security_headers(&response.headers); +} diff --git a/dapps/src/tests/helpers.rs b/dapps/src/tests/helpers.rs index 0069dc31b..1fa2e777a 100644 --- a/dapps/src/tests/helpers.rs +++ b/dapps/src/tests/helpers.rs @@ -69,12 +69,13 @@ fn init_logger() { } } -pub fn init_server(hosts: Option<Vec<String>>) -> (Server, Arc<FakeRegistrar>) { +pub fn init_server(hosts: Option<Vec<String>>, is_syncing: bool) -> (Server, Arc<FakeRegistrar>) { init_logger(); let registrar = Arc::new(FakeRegistrar::new()); let mut dapps_path = env::temp_dir(); dapps_path.push("non-existent-dir-to-prevent-fs-files-from-loading"); let mut builder = ServerBuilder::new(dapps_path.to_str().unwrap().into(), registrar.clone()); + builder.with_sync_status(Arc::new(move || is_syncing)); builder.with_signer_port(Some(SIGNER_PORT)); ( builder.start_unsecured_http(&"127.0.0.1:0".parse().unwrap(), hosts).unwrap(), @@ -93,15 +94,19 @@ pub fn serve_with_auth(user: &str, pass: &str) -> Server { } pub fn serve_hosts(hosts: Option<Vec<String>>) -> Server { - init_server(hosts).0 + init_server(hosts, false).0 } pub fn serve_with_registrar() -> (Server, Arc<FakeRegistrar>) { - init_server(None) + init_server(None, false) +} + +pub fn serve_with_registrar_and_sync() -> (Server, Arc<FakeRegistrar>) { + init_server(None, true) } pub fn serve() -> Server { - init_server(None).0 + init_server(None, false).0 } pub fn request(server: Server, request: &str) -> http_client::Response { From aca82fb84b72633fedb15f207d001316f3de7ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= <tomusdrw@users.noreply.github.com> Date: Sat, 22 Oct 2016 20:07:12 +0200 Subject: [PATCH 09/77] Removing submodule in favour of rust crate (#2756) * Removing submodule * Fixing UI dependency structure. * Merging RS and JS package * Updating release script to push also rs files * fix merge gone wrong * Fixing compilation --- .gitmodules | 3 --- Cargo.lock | 26 +++++++++++++++++++++++--- Cargo.toml | 29 ++++++++++++++++++----------- dapps/Cargo.toml | 9 +++++---- dapps/js-glue/src/js.rs | 6 +++--- dapps/ui/Cargo.toml | 19 +++++++++---------- dapps/ui/src/lib.rs | 19 +++++++++++++++---- js/.gitignore | 1 + js/Cargo.precompiled.toml | 19 +++++++++++++++++++ js/Cargo.toml | 18 ++++++++++++++++++ js/build | 1 - {dapps/ui => js}/build.rs | 2 +- js/scripts/release.sh | 11 +++++++++++ js/scripts/update-precompiled.sh | 16 ++++------------ js/src/lib.rs | 22 ++++++++++++++++++++++ {dapps/ui => js}/src/lib.rs.in | 6 +++--- signer/Cargo.toml | 9 ++++----- signer/src/ws_server/session.rs | 4 ++-- 18 files changed, 158 insertions(+), 62 deletions(-) create mode 100644 js/Cargo.precompiled.toml create mode 100644 js/Cargo.toml delete mode 160000 js/build rename {dapps/ui => js}/build.rs (90%) create mode 100644 js/src/lib.rs rename {dapps/ui => js}/src/lib.rs.in (88%) diff --git a/.gitmodules b/.gitmodules index a3e70108b..06b71f6ae 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,6 +2,3 @@ path = ethcore/res/ethereum/tests url = https://github.com/ethereum/tests.git branch = develop -[submodule "js/build"] - path = js/build - url = https://github.com/ethcore/js-precompiled diff --git a/Cargo.lock b/Cargo.lock index 531557c2f..33581c9e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -341,7 +341,7 @@ dependencies = [ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-dapps-glue 1.4.0", + "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "parity-ui 1.4.0", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", @@ -508,7 +508,7 @@ dependencies = [ "ethcore-util 1.4.0", "jsonrpc-core 3.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-dapps-glue 1.4.0", + "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "parity-ui 1.4.0", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1177,6 +1177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "parity-dapps-glue" version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1191,7 +1192,24 @@ dependencies = [ name = "parity-ui" version = "1.4.0" dependencies = [ - "parity-dapps-glue 1.4.0", + "parity-ui-dev 1.4.0", + "parity-ui-precompiled 1.4.0 (git+https://github.com/ethcore/js-precompiled.git)", + "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "parity-ui-dev" +version = "1.4.0" +dependencies = [ + "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "parity-ui-precompiled" +version = "1.4.0" +source = "git+https://github.com/ethcore/js-precompiled.git#eba8fdcb29c2230868b6604dd341513b5beceba9" +dependencies = [ + "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1993,6 +2011,8 @@ dependencies = [ "checksum number_prefix 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "084d05f4bf60621a9ac9bde941a410df548f4de9545f06e5ee9d3aef4b97cd77" "checksum odds 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "b28c06e81b0f789122d415d6394b5fe849bde8067469f4c2980d3cdc10c78ec1" "checksum owning_ref 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8d91377085359426407a287ab16884a0111ba473aa6844ff01d4ec20ce3d75e7" +"checksum parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "98378dec0a185da2b7180308752f0bad73aaa949c3e0a3b0528d0e067945f7ab" +"checksum parity-ui-precompiled 1.4.0 (git+https://github.com/ethcore/js-precompiled.git)" = "<none>" "checksum parking_lot 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "968f685642555d2f7e202c48b8b11de80569e9bfea817f7f12d7c61aac62d4e6" "checksum parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "dbc5847584161f273e69edc63c1a86254a22f570a0b5dd87aa6f9773f6f7d125" "checksum parking_lot_core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fb1b97670a2ffadce7c397fb80a3d687c4f3060140b885621ef1653d0e5d5068" diff --git a/Cargo.toml b/Cargo.toml index 0173533ee..0eec6ff7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,11 @@ lazy_static = "0.2" regex = "0.1" isatty = "0.1" toml = "0.2" +serde = "0.8.0" +serde_json = "0.8.0" +hyper = { version = "0.9", default-features = false } ctrlc = { git = "https://github.com/ethcore/rust-ctrlc.git" } +json-ipc-server = { git = "https://github.com/ethcore/json-ipc-server.git" } fdlimit = { path = "util/fdlimit" } ethcore = { path = "ethcore" } ethcore-util = { path = "util" } @@ -40,12 +44,9 @@ ethcore-ipc = { path = "ipc/rpc" } ethcore-ipc-hypervisor = { path = "ipc/hypervisor" } ethcore-logger = { path = "logger" } rlp = { path = "util/rlp" } -json-ipc-server = { git = "https://github.com/ethcore/json-ipc-server.git" } +ethcore-stratum = { path = "stratum" } ethcore-dapps = { path = "dapps", optional = true } clippy = { version = "0.0.90", optional = true} -ethcore-stratum = { path = "stratum" } -serde = "0.8.0" -serde_json = "0.8.0" [target.'cfg(windows)'.dependencies] winapi = "0.2" @@ -53,14 +54,20 @@ winapi = "0.2" [target.'cfg(not(windows))'.dependencies] daemonize = "0.2" -[dependencies.hyper] -version = "0.9" -default-features = false - [features] -default = ["ui", "use-precompiled-js"] -ui = ["dapps", "ethcore-signer/ui"] -use-precompiled-js = ["ethcore-dapps/use-precompiled-js", "ethcore-signer/use-precompiled-js"] +default = ["ui-precompiled"] + +ui = [ + "dapps", + "ethcore-dapps/ui", + "ethcore-signer/ui", +] +ui-precompiled = [ + "dapps", + "ethcore-signer/ui-precompiled", + "ethcore-dapps/ui-precompiled", +] + dapps = ["ethcore-dapps"] ipc = ["ethcore/ipc", "ethsync/ipc"] jit = ["ethcore/jit"] diff --git a/dapps/Cargo.toml b/dapps/Cargo.toml index 7ca792d40..9c49c7e28 100644 --- a/dapps/Cargo.toml +++ b/dapps/Cargo.toml @@ -20,17 +20,17 @@ url = "1.0" rustc-serialize = "0.3" serde = "0.8" serde_json = "0.8" -serde_macros = { version = "0.8", optional = true } -zip = { version = "0.1", default-features = false } ethabi = "0.2.2" linked-hash-map = "0.3" +parity-dapps-glue = "1.4" mime = "0.2" +serde_macros = { version = "0.8", optional = true } +zip = { version = "0.1", default-features = false } ethcore-devtools = { path = "../devtools" } ethcore-rpc = { path = "../rpc" } ethcore-util = { path = "../util" } fetch = { path = "../util/fetch" } parity-ui = { path = "./ui" } -parity-dapps-glue = { path = "./js-glue" } mime_guess = { version = "1.6.1" } clippy = { version = "0.0.90", optional = true} @@ -43,4 +43,5 @@ default = ["serde_codegen"] nightly = ["serde_macros"] dev = ["clippy", "ethcore-rpc/dev", "ethcore-util/dev"] -use-precompiled-js = ["parity-ui/use-precompiled-js"] +ui = ["parity-ui/no-precompiled-js"] +ui-precompiled = ["parity-ui/use-precompiled-js"] diff --git a/dapps/js-glue/src/js.rs b/dapps/js-glue/src/js.rs index d3593a306..39c33689c 100644 --- a/dapps/js-glue/src/js.rs +++ b/dapps/js-glue/src/js.rs @@ -53,11 +53,11 @@ fn die<T : fmt::Debug>(s: &'static str, e: T) -> ! { pub fn test(_path: &str) { } #[cfg(feature = "use-precompiled-js")] -pub fn build(_path: &str) { +pub fn build(_path: &str, _dest: &str) { } #[cfg(not(feature = "use-precompiled-js"))] -pub fn build(path: &str) { +pub fn build(path: &str, dest: &str) { let child = platform::handle_fd(&mut Command::new(platform::NPM_CMD)) .arg("install") .arg("--no-progress") @@ -70,7 +70,7 @@ pub fn build(path: &str) { .arg("run") .arg("build") .env("NODE_ENV", "production") - .env("BUILD_DEST", "build") + .env("BUILD_DEST", dest) .current_dir(path) .status() .unwrap_or_else(|e| die("Building JS code", e)); diff --git a/dapps/ui/Cargo.toml b/dapps/ui/Cargo.toml index ff88d0f80..e71782dfa 100644 --- a/dapps/ui/Cargo.toml +++ b/dapps/ui/Cargo.toml @@ -1,19 +1,18 @@ [package] -description = "Parity built-in dapps." +description = "Ethcore Parity UI" +homepage = "http://ethcore.io" +license = "GPL-3.0" name = "parity-ui" version = "1.4.0" -license = "GPL-3.0" authors = ["Ethcore <admin@ethcore.io>"] -build = "build.rs" - -[features] -default = ["with-syntex"] -use-precompiled-js = ["parity-dapps-glue/use-precompiled-js"] -with-syntex = ["parity-dapps-glue/with-syntex"] [build-dependencies] -parity-dapps-glue = { path = "../js-glue" } +rustc_version = "0.1" [dependencies] -parity-dapps-glue = { path = "../js-glue" } +parity-ui-dev = { path = "../../js", optional = true } +parity-ui-precompiled = { git = "https://github.com/ethcore/js-precompiled.git", optional = true } +[features] +no-precompiled-js = ["parity-ui-dev"] +use-precompiled-js = ["parity-ui-precompiled"] diff --git a/dapps/ui/src/lib.rs b/dapps/ui/src/lib.rs index 25d336fab..6a69a503d 100644 --- a/dapps/ui/src/lib.rs +++ b/dapps/ui/src/lib.rs @@ -14,9 +14,20 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. -#[cfg(feature = "with-syntex")] -include!(concat!(env!("OUT_DIR"), "/lib.rs")); -#[cfg(not(feature = "with-syntex"))] -include!("lib.rs.in"); +#[cfg(feature = "parity-ui-dev")] +mod inner { + extern crate parity_ui_dev; + pub use self::parity_ui_dev::*; +} + +#[cfg(feature = "parity-ui-precompiled")] +mod inner { + extern crate parity_ui_precompiled; + + pub use self::parity_ui_precompiled::*; +} + + +pub use self::inner::*; diff --git a/js/.gitignore b/js/.gitignore index c846423fc..f1c885637 100644 --- a/js/.gitignore +++ b/js/.gitignore @@ -1,5 +1,6 @@ node_modules npm-debug.log +build .build .coverage .happypack diff --git a/js/Cargo.precompiled.toml b/js/Cargo.precompiled.toml new file mode 100644 index 000000000..38e0a3b62 --- /dev/null +++ b/js/Cargo.precompiled.toml @@ -0,0 +1,19 @@ +[package] +description = "Parity built-in dapps." +name = "parity-ui-precompiled" +version = "1.4.0" +license = "GPL-3.0" +authors = ["Ethcore <admin@ethcore.io>"] +build = "build.rs" + +[features] +default = ["with-syntex"] +use-precompiled-js = ["parity-dapps-glue/use-precompiled-js"] +with-syntex = ["parity-dapps-glue/with-syntex"] + +[build-dependencies] +parity-dapps-glue = "1.4" + +[dependencies] +parity-dapps-glue = "1.4" + diff --git a/js/Cargo.toml b/js/Cargo.toml new file mode 100644 index 000000000..e52bfec9e --- /dev/null +++ b/js/Cargo.toml @@ -0,0 +1,18 @@ +[package] +description = "Parity built-in dapps." +name = "parity-ui-dev" +version = "1.4.0" +license = "GPL-3.0" +authors = ["Ethcore <admin@ethcore.io>"] +build = "build.rs" + +[features] +default = ["with-syntex"] +with-syntex = ["parity-dapps-glue/with-syntex"] + +[build-dependencies] +parity-dapps-glue = "1.4" + +[dependencies] +parity-dapps-glue = "1.4" + diff --git a/js/build b/js/build deleted file mode 160000 index f94a8eddb..000000000 --- a/js/build +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f94a8eddb8789410dda0db03d4f1d6ae42b31208 diff --git a/dapps/ui/build.rs b/js/build.rs similarity index 90% rename from dapps/ui/build.rs rename to js/build.rs index 395f513bd..82bf1ac93 100644 --- a/dapps/ui/build.rs +++ b/js/build.rs @@ -17,6 +17,6 @@ extern crate parity_dapps_glue; fn main() { - parity_dapps_glue::js::build(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js")); + parity_dapps_glue::js::build(env!("CARGO_MANIFEST_DIR"), "build"); parity_dapps_glue::generate(); } diff --git a/js/scripts/release.sh b/js/scripts/release.sh index 05dc1d784..bff5ea7d9 100755 --- a/js/scripts/release.sh +++ b/js/scripts/release.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e # change into the build directory pushd `dirname $0` @@ -7,6 +8,16 @@ cd ../.build # variables UTCDATE=`date -u "+%Y%m%d-%H%M%S"` +# Create proper directory structure +mkdir -p build +mv * build || true +mkdir -p src + +# Copy rust files +cp ../Cargo.precompiled.toml Cargo.toml +cp ../build.rs . +cp ../src/lib.rs* ./src/ + # init git rm -rf ./.git git init diff --git a/js/scripts/update-precompiled.sh b/js/scripts/update-precompiled.sh index 85548cc14..0b9461bf0 100755 --- a/js/scripts/update-precompiled.sh +++ b/js/scripts/update-precompiled.sh @@ -1,19 +1,11 @@ #!/bin/bash +set -e - -# change into the submodule build directory +# change into main dir pushd `dirname $0` -cd ../build +cd ../../ -if [ -z "$1" ]; then - popd - echo "Usage: $0 <sha-commit>" - exit 1 -fi - -git fetch -git fetch origin $1 -git merge $1 -X theirs +cargo update -p parity-ui-precompiled popd exit 0 diff --git a/js/src/lib.rs b/js/src/lib.rs new file mode 100644 index 000000000..25d336fab --- /dev/null +++ b/js/src/lib.rs @@ -0,0 +1,22 @@ +// 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/>. + +#[cfg(feature = "with-syntex")] +include!(concat!(env!("OUT_DIR"), "/lib.rs")); + +#[cfg(not(feature = "with-syntex"))] +include!("lib.rs.in"); + diff --git a/dapps/ui/src/lib.rs.in b/js/src/lib.rs.in similarity index 88% rename from dapps/ui/src/lib.rs.in rename to js/src/lib.rs.in index 319d17ed3..b3f09556a 100644 --- a/dapps/ui/src/lib.rs.in +++ b/js/src/lib.rs.in @@ -20,7 +20,7 @@ use std::collections::HashMap; use parity_dapps_glue::{WebApp, File, Info}; #[derive(WebAppFiles)] -#[webapp(path = "../../../js/build")] +#[webapp(path = "../build")] pub struct App { pub files: HashMap<&'static str, File>, } @@ -43,7 +43,7 @@ impl WebApp for App { name: "Parity UI", version: env!("CARGO_PKG_VERSION"), author: "Ethcore <admin@ethcore.io>", - description: "New UI for Parity. (Experimental)", + description: "New UI for Parity.", icon_url: "icon.png", } } @@ -51,5 +51,5 @@ impl WebApp for App { #[test] fn test_js() { - parity_dapps_glue::js::build(concat!(env!("CARGO_MANIFEST_DIR"), "/../../js")); + parity_dapps_glue::js::build(env!("CARGO_MANIFEST_DIR")); } diff --git a/signer/Cargo.toml b/signer/Cargo.toml index 892b5009c..b8a7c5ce4 100644 --- a/signer/Cargo.toml +++ b/signer/Cargo.toml @@ -15,18 +15,17 @@ rand = "0.3.14" jsonrpc-core = "3.0" log = "0.3" env_logger = "0.3" +parity-dapps-glue = { version = "1.4", optional = true } ws = { git = "https://github.com/ethcore/ws-rs.git", branch = "mio-upstream-stable" } ethcore-util = { path = "../util" } ethcore-io = { path = "../util/io" } ethcore-rpc = { path = "../rpc" } ethcore-devtools = { path = "../devtools" } -parity-dapps-glue = { path = "../dapps/js-glue", version = "1.4", optional = true} -parity-ui = { path = "../dapps/ui", version = "1.4", optional = true} +parity-ui = { path = "../dapps/ui", version = "1.4", optional = true } clippy = { version = "0.0.90", optional = true} [features] dev = ["clippy"] -ui = ["parity-dapps-glue", "parity-ui"] -use-precompiled-js = ["parity-ui/use-precompiled-js"] - +ui = ["parity-dapps-glue", "parity-ui", "parity-ui/no-precompiled-js"] +ui-precompiled = ["parity-dapps-glue", "parity-ui", "parity-ui/use-precompiled-js"] diff --git a/signer/src/ws_server/session.rs b/signer/src/ws_server/session.rs index 2ff2bc10f..b99ef48ef 100644 --- a/signer/src/ws_server/session.rs +++ b/signer/src/ws_server/session.rs @@ -24,7 +24,7 @@ use std::str::FromStr; use jsonrpc_core::IoHandler; use util::{H256, Mutex, version}; -#[cfg(feature = "ui")] +#[cfg(feature = "parity-ui")] mod ui { extern crate parity_ui as ui; extern crate parity_dapps_glue as dapps; @@ -46,7 +46,7 @@ mod ui { } } } -#[cfg(not(feature = "ui"))] +#[cfg(not(feature = "parity-ui"))] mod ui { pub struct File { pub content: &'static [u8], From 3637c6ad9adb9f7641c24a854d00f3af9e17f5b3 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Sat, 22 Oct 2016 22:29:47 +0200 Subject: [PATCH 10/77] Adjust network name badge colours (darker) (#2823) --- js/src/views/Application/Status/status.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/src/views/Application/Status/status.css b/js/src/views/Application/Status/status.css index 22d119eb5..db97908bc 100644 --- a/js/src/views/Application/Status/status.css +++ b/js/src/views/Application/Status/status.css @@ -51,11 +51,11 @@ } .networklive { - background: rgb(75, 255, 75); + background: rgb(0, 136, 0); } .networktest { - background: rgb(255, 75, 75); + background: rgb(136, 0, 0); } .peers { From 8cf9934cab6f0dbd422484053a010308f904b7a2 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Sun, 23 Oct 2016 00:28:19 +0200 Subject: [PATCH 11/77] Update build to working version on repo (#2825) --- js/build.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/js/build.rs b/js/build.rs index 82bf1ac93..0048b7d5a 100644 --- a/js/build.rs +++ b/js/build.rs @@ -17,6 +17,10 @@ extern crate parity_dapps_glue; fn main() { - parity_dapps_glue::js::build(env!("CARGO_MANIFEST_DIR"), "build"); + // FIXME: Currently creates an issue when + // (a) always trying to build & + // (b) trying to install node_modules inside the build directory (no package.json) + // Uncomment the next line when fixed to perfection + // parity_dapps_glue::js::build(env!("CARGO_MANIFEST_DIR"), "build"); parity_dapps_glue::generate(); } From bd040d4cfd2073d0571e59471adca70a79e7f01d Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac <ngotchac@gmail.com> Date: Mon, 24 Oct 2016 10:28:18 +0200 Subject: [PATCH 12/77] Fix case error in Dapps import --- js/src/views/Dapps/AddDapps/AddDapps.js | 2 +- js/src/views/Dapps/AddDapps/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/src/views/Dapps/AddDapps/AddDapps.js b/js/src/views/Dapps/AddDapps/AddDapps.js index 04cdfeba3..208a65004 100644 --- a/js/src/views/Dapps/AddDapps/AddDapps.js +++ b/js/src/views/Dapps/AddDapps/AddDapps.js @@ -21,7 +21,7 @@ import Checkbox from 'material-ui/Checkbox'; import { Modal, Button } from '../../../ui'; -import styles from './addDapps.css'; +import styles from './AddDapps.css'; export default class AddDapps extends Component { static propTypes = { diff --git a/js/src/views/Dapps/AddDapps/index.js b/js/src/views/Dapps/AddDapps/index.js index 383c648cf..6014c7315 100644 --- a/js/src/views/Dapps/AddDapps/index.js +++ b/js/src/views/Dapps/AddDapps/index.js @@ -14,4 +14,4 @@ // 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 './addDapps'; +export default from './AddDapps'; From e5f86c62adffced6be8dcd59ffa646ab68cb0001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= <tomusdrw@users.noreply.github.com> Date: Mon, 24 Oct 2016 12:21:15 +0200 Subject: [PATCH 13/77] Dapps port RPC (#2819) --- parity/dapps.rs | 2 +- parity/rpc_apis.rs | 22 +- parity/run.rs | 7 +- rpc/src/v1/helpers/errors.rs | 9 + rpc/src/v1/helpers/signer.rs | 18 +- rpc/src/v1/impls/ethcore.rs | 28 ++- rpc/src/v1/impls/personal.rs | 13 +- rpc/src/v1/tests/mocked/eth_signing.rs | 8 +- rpc/src/v1/tests/mocked/ethcore.rs | 277 ++++++++++----------- rpc/src/v1/tests/mocked/personal.rs | 46 +--- rpc/src/v1/tests/mocked/personal_signer.rs | 2 +- rpc/src/v1/traits/ethcore.rs | 10 +- rpc/src/v1/traits/personal.rs | 4 - 13 files changed, 223 insertions(+), 223 deletions(-) diff --git a/parity/dapps.rs b/parity/dapps.rs index d7bb0c07a..46ac1ec60 100644 --- a/parity/dapps.rs +++ b/parity/dapps.rs @@ -58,7 +58,7 @@ pub fn new(configuration: Configuration, deps: Dependencies) -> Result<Option<We return Ok(None); } - let signer_port = deps.apis.signer_port.clone(); + let signer_port = deps.apis.signer_service.port(); let url = format!("{}:{}", configuration.interface, configuration.port); let addr = try!(url.parse().map_err(|_| format!("Invalid Webapps listen host/port given: {}", url))); diff --git a/parity/rpc_apis.rs b/parity/rpc_apis.rs index 29b33b844..825e2db3a 100644 --- a/parity/rpc_apis.rs +++ b/parity/rpc_apis.rs @@ -93,7 +93,6 @@ impl FromStr for ApiSet { } pub struct Dependencies { - pub signer_port: Option<u16>, pub signer_service: Arc<SignerService>, pub client: Arc<Client>, pub sync: Arc<SyncProvider>, @@ -105,6 +104,7 @@ pub struct Dependencies { pub settings: Arc<NetworkSettings>, pub net_service: Arc<ManageNetwork>, pub geth_compatibility: bool, + pub dapps_port: Option<u16>, } fn to_modules(apis: &[Api]) -> BTreeMap<String, String> { @@ -172,21 +172,33 @@ pub fn setup_rpc<T: Extendable>(server: T, deps: Arc<Dependencies>, apis: ApiSet let filter_client = EthFilterClient::new(&deps.client, &deps.miner); server.add_delegate(filter_client.to_delegate()); - if deps.signer_port.is_some() { + if deps.signer_service.is_enabled() { server.add_delegate(EthSigningQueueClient::new(&deps.signer_service, &deps.client, &deps.miner, &deps.secret_store).to_delegate()); } else { server.add_delegate(EthSigningUnsafeClient::new(&deps.client, &deps.secret_store, &deps.miner).to_delegate()); } }, Api::Personal => { - server.add_delegate(PersonalClient::new(&deps.secret_store, &deps.client, &deps.miner, deps.signer_port, deps.geth_compatibility).to_delegate()); + server.add_delegate(PersonalClient::new(&deps.secret_store, &deps.client, &deps.miner, deps.geth_compatibility).to_delegate()); }, Api::Signer => { server.add_delegate(SignerClient::new(&deps.secret_store, &deps.client, &deps.miner, &deps.signer_service).to_delegate()); }, Api::Ethcore => { - let signer = deps.signer_port.map(|_| deps.signer_service.clone()); - server.add_delegate(EthcoreClient::new(&deps.client, &deps.miner, &deps.sync, &deps.net_service, deps.logger.clone(), deps.settings.clone(), signer).to_delegate()) + let signer = match deps.signer_service.is_enabled() { + true => Some(deps.signer_service.clone()), + false => None, + }; + server.add_delegate(EthcoreClient::new( + &deps.client, + &deps.miner, + &deps.sync, + &deps.net_service, + deps.logger.clone(), + deps.settings.clone(), + signer, + deps.dapps_port, + ).to_delegate()) }, Api::EthcoreSet => { server.add_delegate(EthcoreSetClient::new(&deps.client, &deps.miner, &deps.net_service).to_delegate()) diff --git a/parity/run.rs b/parity/run.rs index 9d0caf751..46cc543f2 100644 --- a/parity/run.rs +++ b/parity/run.rs @@ -246,10 +246,9 @@ pub fn execute(cmd: RunCmd) -> Result<(), String> { // set up dependencies for rpc servers let signer_path = cmd.signer_conf.signer_path.clone(); let deps_for_rpc_apis = Arc::new(rpc_apis::Dependencies { - signer_port: cmd.signer_port, signer_service: Arc::new(rpc_apis::SignerService::new(move || { signer::generate_new_token(signer_path.clone()).map_err(|e| format!("{:?}", e)) - })), + }, cmd.signer_port)), client: client.clone(), sync: sync_provider.clone(), net: manage_network.clone(), @@ -260,6 +259,10 @@ pub fn execute(cmd: RunCmd) -> Result<(), String> { settings: Arc::new(cmd.net_settings.clone()), net_service: manage_network.clone(), geth_compatibility: cmd.geth_compatibility, + dapps_port: match cmd.dapps_conf.enabled { + true => Some(cmd.dapps_conf.port), + false => None, + }, }); let dependencies = rpc::Dependencies { diff --git a/rpc/src/v1/helpers/errors.rs b/rpc/src/v1/helpers/errors.rs index c54cd9c34..572adca3a 100644 --- a/rpc/src/v1/helpers/errors.rs +++ b/rpc/src/v1/helpers/errors.rs @@ -39,6 +39,7 @@ mod codes { pub const PASSWORD_INVALID: i64 = -32021; pub const ACCOUNT_ERROR: i64 = -32023; pub const SIGNER_DISABLED: i64 = -32030; + pub const DAPPS_DISABLED: i64 = -32031; pub const REQUEST_REJECTED: i64 = -32040; pub const REQUEST_REJECTED_LIMIT: i64 = -32041; pub const REQUEST_NOT_FOUND: i64 = -32042; @@ -167,6 +168,14 @@ pub fn signer_disabled() -> Error { } } +pub fn dapps_disabled() -> Error { + Error { + code: ErrorCode::ServerError(codes::DAPPS_DISABLED), + message: "Dapps Server is disabled. This API is not available.".into(), + data: None + } +} + pub fn encryption_error<T: fmt::Debug>(error: T) -> Error { Error { code: ErrorCode::ServerError(codes::ENCRYPTION_ERROR), diff --git a/rpc/src/v1/helpers/signer.rs b/rpc/src/v1/helpers/signer.rs index 2cebc8261..d4a5af273 100644 --- a/rpc/src/v1/helpers/signer.rs +++ b/rpc/src/v1/helpers/signer.rs @@ -22,16 +22,18 @@ use v1::helpers::signing_queue::{ConfirmationsQueue}; pub struct SignerService { queue: Arc<ConfirmationsQueue>, generate_new_token: Box<Fn() -> Result<String, String> + Send + Sync + 'static>, + port: Option<u16>, } impl SignerService { /// Creates new Signer Service given function to generate new tokens. - pub fn new<F>(new_token: F) -> Self + pub fn new<F>(new_token: F, port: Option<u16>) -> Self where F: Fn() -> Result<String, String> + Send + Sync + 'static { SignerService { queue: Arc::new(ConfirmationsQueue::default()), generate_new_token: Box::new(new_token), + port: port, } } @@ -45,10 +47,20 @@ impl SignerService { self.queue.clone() } + /// Returns signer port (if signer enabled) or `None` otherwise + pub fn port(&self) -> Option<u16> { + self.port + } + + /// Returns true if Signer is enabled. + pub fn is_enabled(&self) -> bool { + self.port.is_some() + } + #[cfg(test)] /// Creates new Signer Service for tests. - pub fn new_test() -> Self { - SignerService::new(|| Ok("new_token".into())) + pub fn new_test(port: Option<u16>) -> Self { + SignerService::new(|| Ok("new_token".into()), port) } } diff --git a/rpc/src/v1/impls/ethcore.rs b/rpc/src/v1/impls/ethcore.rs index b430a710c..1a1410ebd 100644 --- a/rpc/src/v1/impls/ethcore.rs +++ b/rpc/src/v1/impls/ethcore.rs @@ -52,7 +52,8 @@ pub struct EthcoreClient<C, M, S: ?Sized, F=FetchClient> where logger: Arc<RotatingLogger>, settings: Arc<NetworkSettings>, signer: Option<Arc<SignerService>>, - fetch: Mutex<F> + fetch: Mutex<F>, + dapps_port: Option<u16>, } impl<C, M, S: ?Sized> EthcoreClient<C, M, S> where @@ -67,9 +68,10 @@ impl<C, M, S: ?Sized> EthcoreClient<C, M, S> where net: &Arc<ManageNetwork>, logger: Arc<RotatingLogger>, settings: Arc<NetworkSettings>, - signer: Option<Arc<SignerService>> + signer: Option<Arc<SignerService>>, + dapps_port: Option<u16>, ) -> Self { - Self::with_fetch(client, miner, sync, net, logger, settings, signer) + Self::with_fetch(client, miner, sync, net, logger, settings, signer, dapps_port) } } @@ -87,7 +89,8 @@ impl<C, M, S: ?Sized, F> EthcoreClient<C, M, S, F> where net: &Arc<ManageNetwork>, logger: Arc<RotatingLogger>, settings: Arc<NetworkSettings>, - signer: Option<Arc<SignerService>> + signer: Option<Arc<SignerService>>, + dapps_port: Option<u16>, ) -> Self { EthcoreClient { client: Arc::downgrade(client), @@ -98,6 +101,7 @@ impl<C, M, S: ?Sized, F> EthcoreClient<C, M, S, F> where settings: settings, signer: signer, fetch: Mutex::new(F::default()), + dapps_port: dapps_port, } } @@ -314,4 +318,20 @@ impl<C, M, S: ?Sized, F> Ethcore for EthcoreClient<C, M, S, F> where } } } + + fn signer_port(&self) -> Result<u16, Error> { + try!(self.active()); + + self.signer + .clone() + .and_then(|signer| signer.port()) + .ok_or_else(|| errors::signer_disabled()) + } + + fn dapps_port(&self) -> Result<u16, Error> { + try!(self.active()); + + self.dapps_port + .ok_or_else(|| errors::dapps_disabled()) + } } diff --git a/rpc/src/v1/impls/personal.rs b/rpc/src/v1/impls/personal.rs index fde5f10b2..0d6b63240 100644 --- a/rpc/src/v1/impls/personal.rs +++ b/rpc/src/v1/impls/personal.rs @@ -34,18 +34,16 @@ pub struct PersonalClient<C, M> where C: MiningBlockChainClient, M: MinerService accounts: Weak<AccountProvider>, client: Weak<C>, miner: Weak<M>, - signer_port: Option<u16>, allow_perm_unlock: bool, } impl<C, M> PersonalClient<C, M> where C: MiningBlockChainClient, M: MinerService { /// Creates new PersonalClient - pub fn new(store: &Arc<AccountProvider>, client: &Arc<C>, miner: &Arc<M>, signer_port: Option<u16>, allow_perm_unlock: bool) -> Self { + pub fn new(store: &Arc<AccountProvider>, client: &Arc<C>, miner: &Arc<M>, allow_perm_unlock: bool) -> Self { PersonalClient { accounts: Arc::downgrade(store), client: Arc::downgrade(client), miner: Arc::downgrade(miner), - signer_port: signer_port, allow_perm_unlock: allow_perm_unlock, } } @@ -59,15 +57,6 @@ impl<C, M> PersonalClient<C, M> where C: MiningBlockChainClient, M: MinerService impl<C: 'static, M: 'static> Personal for PersonalClient<C, M> where C: MiningBlockChainClient, M: MinerService { - fn signer_enabled(&self, params: Params) -> Result<Value, Error> { - try!(self.active()); - try!(expect_no_params(params)); - - Ok(self.signer_port - .map(|v| to_value(&v)) - .unwrap_or_else(|| to_value(&false))) - } - fn accounts(&self, params: Params) -> Result<Value, Error> { try!(self.active()); try!(expect_no_params(params)); diff --git a/rpc/src/v1/tests/mocked/eth_signing.rs b/rpc/src/v1/tests/mocked/eth_signing.rs index fc5800cd7..58b5e0546 100644 --- a/rpc/src/v1/tests/mocked/eth_signing.rs +++ b/rpc/src/v1/tests/mocked/eth_signing.rs @@ -41,7 +41,7 @@ struct EthSigningTester { impl Default for EthSigningTester { fn default() -> Self { - let signer = Arc::new(SignerService::new_test()); + let signer = Arc::new(SignerService::new_test(None)); let client = Arc::new(TestBlockChainClient::default()); let miner = Arc::new(TestMinerService::default()); let accounts = Arc::new(AccountProvider::transient_provider()); @@ -272,10 +272,8 @@ fn should_dispatch_transaction_if_account_is_unlock() { fn should_decrypt_message_if_account_is_unlocked() { // given let tester = eth_signing(); - let sync = ethcore::sync_provider(); - let net = ethcore::network_service(); - let ethcore_client = ethcore::ethcore_client(&tester.client, &tester.miner, &sync, &net); - tester.io.add_delegate(ethcore_client.to_delegate()); + let ethcore = ethcore::Dependencies::new(); + tester.io.add_delegate(ethcore.client(None).to_delegate()); let (address, public) = tester.accounts.new_account_and_public("test").unwrap(); tester.accounts.unlock_account_permanently(address, "test".into()).unwrap(); diff --git a/rpc/src/v1/tests/mocked/ethcore.rs b/rpc/src/v1/tests/mocked/ethcore.rs index 4cf23d46e..ea4112c19 100644 --- a/rpc/src/v1/tests/mocked/ethcore.rs +++ b/rpc/src/v1/tests/mocked/ethcore.rs @@ -27,59 +27,72 @@ use v1::helpers::{SignerService, NetworkSettings}; use v1::tests::helpers::{TestSyncProvider, Config, TestMinerService, TestFetch}; use super::manage_network::TestManageNetwork; -fn miner_service() -> Arc<TestMinerService> { - Arc::new(TestMinerService::default()) -} - -fn client_service() -> Arc<TestBlockChainClient> { - Arc::new(TestBlockChainClient::default()) -} - -pub fn sync_provider() -> Arc<TestSyncProvider> { - Arc::new(TestSyncProvider::new(Config { - network_id: U256::from(3), - num_peers: 120, - })) -} - -fn logger() -> Arc<RotatingLogger> { - Arc::new(RotatingLogger::new("rpc=trace".to_owned())) -} - -fn settings() -> Arc<NetworkSettings> { - Arc::new(NetworkSettings { - name: "mynode".to_owned(), - chain: "testchain".to_owned(), - network_port: 30303, - rpc_enabled: true, - rpc_interface: "all".to_owned(), - rpc_port: 8545, - }) -} - -pub fn network_service() -> Arc<ManageNetwork> { - Arc::new(TestManageNetwork) -} pub type TestEthcoreClient = EthcoreClient<TestBlockChainClient, TestMinerService, TestSyncProvider, TestFetch>; -pub fn ethcore_client( - client: &Arc<TestBlockChainClient>, - miner: &Arc<TestMinerService>, - sync: &Arc<TestSyncProvider>, - net: &Arc<ManageNetwork>) - -> TestEthcoreClient { - EthcoreClient::with_fetch(client, miner, sync, net, logger(), settings(), None) +pub struct Dependencies { + pub miner: Arc<TestMinerService>, + pub client: Arc<TestBlockChainClient>, + pub sync: Arc<TestSyncProvider>, + pub logger: Arc<RotatingLogger>, + pub settings: Arc<NetworkSettings>, + pub network: Arc<ManageNetwork>, + pub dapps_port: Option<u16>, +} + +impl Dependencies { + pub fn new() -> Self { + Dependencies { + miner: Arc::new(TestMinerService::default()), + client: Arc::new(TestBlockChainClient::default()), + sync: Arc::new(TestSyncProvider::new(Config { + network_id: U256::from(3), + num_peers: 120, + })), + logger: Arc::new(RotatingLogger::new("rpc=trace".to_owned())), + settings: Arc::new(NetworkSettings { + name: "mynode".to_owned(), + chain: "testchain".to_owned(), + network_port: 30303, + rpc_enabled: true, + rpc_interface: "all".to_owned(), + rpc_port: 8545, + }), + network: Arc::new(TestManageNetwork), + dapps_port: Some(18080), + } + } + + pub fn client(&self, signer: Option<Arc<SignerService>>) -> TestEthcoreClient { + EthcoreClient::with_fetch( + &self.client, + &self.miner, + &self.sync, + &self.network, + self.logger.clone(), + self.settings.clone(), + signer, + self.dapps_port, + ) + } + + fn default_client(&self) -> IoHandler { + let io = IoHandler::new(); + io.add_delegate(self.client(None).to_delegate()); + io + } + + fn with_signer(&self, signer: SignerService) -> IoHandler { + let io = IoHandler::new(); + io.add_delegate(self.client(Some(Arc::new(signer))).to_delegate()); + io + } } #[test] fn rpc_ethcore_extra_data() { - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let io = IoHandler::new(); - io.add_delegate(ethcore_client(&client, &miner, &sync, &net).to_delegate()); + let deps = Dependencies::new(); + let io = deps.default_client(); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_extraData", "params": [], "id": 1}"#; let response = r#"{"jsonrpc":"2.0","result":"0x01020304","id":1}"#; @@ -92,12 +105,8 @@ fn rpc_ethcore_default_extra_data() { use util::misc; use util::ToPretty; - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let io = IoHandler::new(); - io.add_delegate(ethcore_client(&client, &miner, &sync, &net).to_delegate()); + let deps = Dependencies::new(); + let io = deps.default_client(); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_defaultExtraData", "params": [], "id": 1}"#; let response = format!(r#"{{"jsonrpc":"2.0","result":"0x{}","id":1}}"#, misc::version_data().to_hex()); @@ -107,12 +116,8 @@ fn rpc_ethcore_default_extra_data() { #[test] fn rpc_ethcore_gas_floor_target() { - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let io = IoHandler::new(); - io.add_delegate(ethcore_client(&client, &miner, &sync, &net).to_delegate()); + let deps = Dependencies::new(); + let io = deps.default_client(); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_gasFloorTarget", "params": [], "id": 1}"#; let response = r#"{"jsonrpc":"2.0","result":"0x3039","id":1}"#; @@ -122,12 +127,8 @@ fn rpc_ethcore_gas_floor_target() { #[test] fn rpc_ethcore_min_gas_price() { - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let io = IoHandler::new(); - io.add_delegate(ethcore_client(&client, &miner, &sync, &net).to_delegate()); + let deps = Dependencies::new(); + let io = deps.default_client(); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_minGasPrice", "params": [], "id": 1}"#; let response = r#"{"jsonrpc":"2.0","result":"0x1312d00","id":1}"#; @@ -137,16 +138,11 @@ fn rpc_ethcore_min_gas_price() { #[test] fn rpc_ethcore_dev_logs() { - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let logger = logger(); - logger.append("a".to_owned()); - logger.append("b".to_owned()); - let ethcore: TestEthcoreClient = EthcoreClient::with_fetch(&client, &miner, &sync, &net, logger.clone(), settings(), None); - let io = IoHandler::new(); - io.add_delegate(ethcore.to_delegate()); + let deps = Dependencies::new(); + deps.logger.append("a".to_owned()); + deps.logger.append("b".to_owned()); + + let io = deps.default_client(); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_devLogs", "params":[], "id": 1}"#; let response = r#"{"jsonrpc":"2.0","result":["b","a"],"id":1}"#; @@ -156,12 +152,8 @@ fn rpc_ethcore_dev_logs() { #[test] fn rpc_ethcore_dev_logs_levels() { - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let io = IoHandler::new(); - io.add_delegate(ethcore_client(&client, &miner, &sync, &net).to_delegate()); + let deps = Dependencies::new(); + let io = deps.default_client(); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_devLogsLevels", "params":[], "id": 1}"#; let response = r#"{"jsonrpc":"2.0","result":"rpc=trace","id":1}"#; @@ -171,12 +163,8 @@ fn rpc_ethcore_dev_logs_levels() { #[test] fn rpc_ethcore_transactions_limit() { - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let io = IoHandler::new(); - io.add_delegate(ethcore_client(&client, &miner, &sync, &net).to_delegate()); + let deps = Dependencies::new(); + let io = deps.default_client(); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_transactionsLimit", "params":[], "id": 1}"#; let response = r#"{"jsonrpc":"2.0","result":1024,"id":1}"#; @@ -186,12 +174,8 @@ fn rpc_ethcore_transactions_limit() { #[test] fn rpc_ethcore_net_chain() { - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let io = IoHandler::new(); - io.add_delegate(ethcore_client(&client, &miner, &sync, &net).to_delegate()); + let deps = Dependencies::new(); + let io = deps.default_client(); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_netChain", "params":[], "id": 1}"#; let response = r#"{"jsonrpc":"2.0","result":"testchain","id":1}"#; @@ -201,12 +185,8 @@ fn rpc_ethcore_net_chain() { #[test] fn rpc_ethcore_net_peers() { - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let io = IoHandler::new(); - io.add_delegate(ethcore_client(&client, &miner, &sync, &net).to_delegate()); + let deps = Dependencies::new(); + let io = deps.default_client(); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_netPeers", "params":[], "id": 1}"#; let response = "{\"jsonrpc\":\"2.0\",\"result\":{\"active\":0,\"connected\":120,\"max\":50,\"peers\":[{\"caps\":[\"eth/62\",\"eth/63\"],\ @@ -221,12 +201,8 @@ fn rpc_ethcore_net_peers() { #[test] fn rpc_ethcore_net_port() { - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let io = IoHandler::new(); - io.add_delegate(ethcore_client(&client, &miner, &sync, &net).to_delegate()); + let deps = Dependencies::new(); + let io = deps.default_client(); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_netPort", "params":[], "id": 1}"#; let response = r#"{"jsonrpc":"2.0","result":30303,"id":1}"#; @@ -236,12 +212,8 @@ fn rpc_ethcore_net_port() { #[test] fn rpc_ethcore_rpc_settings() { - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let io = IoHandler::new(); - io.add_delegate(ethcore_client(&client, &miner, &sync, &net).to_delegate()); + let deps = Dependencies::new(); + let io = deps.default_client(); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_rpcSettings", "params":[], "id": 1}"#; let response = r#"{"jsonrpc":"2.0","result":{"enabled":true,"interface":"all","port":8545},"id":1}"#; @@ -251,12 +223,8 @@ fn rpc_ethcore_rpc_settings() { #[test] fn rpc_ethcore_node_name() { - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let io = IoHandler::new(); - io.add_delegate(ethcore_client(&client, &miner, &sync, &net).to_delegate()); + let deps = Dependencies::new(); + let io = deps.default_client(); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_nodeName", "params":[], "id": 1}"#; let response = r#"{"jsonrpc":"2.0","result":"mynode","id":1}"#; @@ -266,14 +234,8 @@ fn rpc_ethcore_node_name() { #[test] fn rpc_ethcore_unsigned_transactions_count() { - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let io = IoHandler::new(); - let signer = Arc::new(SignerService::new_test()); - let ethcore: TestEthcoreClient = EthcoreClient::with_fetch(&client, &miner, &sync, &net, logger(), settings(), Some(signer)); - io.add_delegate(ethcore.to_delegate()); + let deps = Dependencies::new(); + let io = deps.with_signer(SignerService::new_test(Some(18180))); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_unsignedTransactionsCount", "params":[], "id": 1}"#; let response = r#"{"jsonrpc":"2.0","result":0,"id":1}"#; @@ -283,12 +245,8 @@ fn rpc_ethcore_unsigned_transactions_count() { #[test] fn rpc_ethcore_unsigned_transactions_count_when_signer_disabled() { - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let io = IoHandler::new(); - io.add_delegate(ethcore_client(&client, &miner, &sync, &net).to_delegate()); + let deps = Dependencies::new(); + let io = deps.default_client(); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_unsignedTransactionsCount", "params":[], "id": 1}"#; let response = r#"{"jsonrpc":"2.0","error":{"code":-32030,"message":"Trusted Signer is disabled. This API is not available.","data":null},"id":1}"#; @@ -298,12 +256,8 @@ fn rpc_ethcore_unsigned_transactions_count_when_signer_disabled() { #[test] fn rpc_ethcore_hash_content() { - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let io = IoHandler::new(); - io.add_delegate(ethcore_client(&client, &miner, &sync, &net).to_delegate()); + let deps = Dependencies::new(); + let io = deps.default_client(); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_hashContent", "params":["https://ethcore.io/assets/images/ethcore-black-horizontal.png"], "id": 1}"#; let response = r#"{"jsonrpc":"2.0","result":"0x2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e","id":1}"#; @@ -313,12 +267,8 @@ fn rpc_ethcore_hash_content() { #[test] fn rpc_ethcore_pending_transactions() { - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let io = IoHandler::new(); - io.add_delegate(ethcore_client(&client, &miner, &sync, &net).to_delegate()); + let deps = Dependencies::new(); + let io = deps.default_client(); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_pendingTransactions", "params":[], "id": 1}"#; let response = r#"{"jsonrpc":"2.0","result":[],"id":1}"#; @@ -328,14 +278,45 @@ fn rpc_ethcore_pending_transactions() { #[test] fn rpc_ethcore_encrypt() { - let miner = miner_service(); - let client = client_service(); - let sync = sync_provider(); - let net = network_service(); - let io = IoHandler::new(); - io.add_delegate(ethcore_client(&client, &miner, &sync, &net).to_delegate()); + let deps = Dependencies::new(); + let io = deps.default_client(); let key = format!("{:?}", Random.generate().unwrap().public()); let request = r#"{"jsonrpc": "2.0", "method": "ethcore_encryptMessage", "params":["0x"#.to_owned() + &key + r#"", "0x01"], "id": 1}"#; assert!(io.handle_request_sync(&request).unwrap().contains("result"), "Should return success."); } + +#[test] +fn rpc_ethcore_signer_port() { + // given + let deps = Dependencies::new(); + let io1 = deps.with_signer(SignerService::new_test(Some(18180))); + let io2 = deps.default_client(); + + // when + let request = r#"{"jsonrpc": "2.0", "method": "ethcore_signerPort", "params": [], "id": 1}"#; + let response1 = r#"{"jsonrpc":"2.0","result":18180,"id":1}"#; + let response2 = r#"{"jsonrpc":"2.0","error":{"code":-32030,"message":"Trusted Signer is disabled. This API is not available.","data":null},"id":1}"#; + + // then + assert_eq!(io1.handle_request_sync(request), Some(response1.to_owned())); + assert_eq!(io2.handle_request_sync(request), Some(response2.to_owned())); +} + +#[test] +fn rpc_ethcore_dapps_port() { + // given + let mut deps = Dependencies::new(); + let io1 = deps.default_client(); + deps.dapps_port = None; + let io2 = deps.default_client(); + + // when + let request = r#"{"jsonrpc": "2.0", "method": "ethcore_dappsPort", "params": [], "id": 1}"#; + let response1 = r#"{"jsonrpc":"2.0","result":18080,"id":1}"#; + let response2 = r#"{"jsonrpc":"2.0","error":{"code":-32031,"message":"Dapps Server is disabled. This API is not available.","data":null},"id":1}"#; + + // then + assert_eq!(io1.handle_request_sync(request), Some(response1.to_owned())); + assert_eq!(io2.handle_request_sync(request), Some(response2.to_owned())); +} diff --git a/rpc/src/v1/tests/mocked/personal.rs b/rpc/src/v1/tests/mocked/personal.rs index c3c3d2954..91e1ef0f5 100644 --- a/rpc/src/v1/tests/mocked/personal.rs +++ b/rpc/src/v1/tests/mocked/personal.rs @@ -46,11 +46,11 @@ fn miner_service() -> Arc<TestMinerService> { Arc::new(TestMinerService::default()) } -fn setup(signer: Option<u16>) -> PersonalTester { +fn setup() -> PersonalTester { let accounts = accounts_provider(); let client = blockchain_client(); let miner = miner_service(); - let personal = PersonalClient::new(&accounts, &client, &miner, signer, false); + let personal = PersonalClient::new(&accounts, &client, &miner, false); let io = IoHandler::new(); io.add_delegate(personal.to_delegate()); @@ -65,37 +65,9 @@ fn setup(signer: Option<u16>) -> PersonalTester { tester } -#[test] -fn should_return_false_if_signer_is_disabled() { - // given - let tester = setup(None); - - // when - let request = r#"{"jsonrpc": "2.0", "method": "personal_signerEnabled", "params": [], "id": 1}"#; - let response = r#"{"jsonrpc":"2.0","result":false,"id":1}"#; - - - // then - assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned())); -} - -#[test] -fn should_return_port_number_if_signer_is_enabled() { - // given - let tester = setup(Some(8180)); - - // when - let request = r#"{"jsonrpc": "2.0", "method": "personal_signerEnabled", "params": [], "id": 1}"#; - let response = r#"{"jsonrpc":"2.0","result":8180,"id":1}"#; - - - // then - assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned())); -} - #[test] fn accounts() { - let tester = setup(None); + let tester = setup(); let address = tester.accounts.new_account("").unwrap(); let request = r#"{"jsonrpc": "2.0", "method": "personal_listAccounts", "params": [], "id": 1}"#; let response = r#"{"jsonrpc":"2.0","result":[""#.to_owned() + &format!("0x{:?}", address) + r#""],"id":1}"#; @@ -105,7 +77,7 @@ fn accounts() { #[test] fn new_account() { - let tester = setup(None); + let tester = setup(); let request = r#"{"jsonrpc": "2.0", "method": "personal_newAccount", "params": ["pass"], "id": 1}"#; let res = tester.io.handle_request_sync(request); @@ -120,7 +92,7 @@ fn new_account() { #[test] fn should_be_able_to_get_account_info() { - let tester = setup(None); + let tester = setup(); tester.accounts.new_account("").unwrap(); let accounts = tester.accounts.accounts().unwrap(); assert_eq!(accounts.len(), 1); @@ -138,7 +110,7 @@ fn should_be_able_to_get_account_info() { #[test] fn should_be_able_to_set_name() { - let tester = setup(None); + let tester = setup(); tester.accounts.new_account("").unwrap(); let accounts = tester.accounts.accounts().unwrap(); assert_eq!(accounts.len(), 1); @@ -159,7 +131,7 @@ fn should_be_able_to_set_name() { #[test] fn should_be_able_to_set_meta() { - let tester = setup(None); + let tester = setup(); tester.accounts.new_account("").unwrap(); let accounts = tester.accounts.accounts().unwrap(); assert_eq!(accounts.len(), 1); @@ -180,7 +152,7 @@ fn should_be_able_to_set_meta() { #[test] fn sign_and_send_transaction_with_invalid_password() { - let tester = setup(None); + let tester = setup(); let address = tester.accounts.new_account("password123").unwrap(); let request = r#"{ "jsonrpc": "2.0", @@ -202,7 +174,7 @@ fn sign_and_send_transaction_with_invalid_password() { #[test] fn sign_and_send_transaction() { - let tester = setup(None); + let tester = setup(); let address = tester.accounts.new_account("password123").unwrap(); let request = r#"{ diff --git a/rpc/src/v1/tests/mocked/personal_signer.rs b/rpc/src/v1/tests/mocked/personal_signer.rs index 650f16553..7b3e18b11 100644 --- a/rpc/src/v1/tests/mocked/personal_signer.rs +++ b/rpc/src/v1/tests/mocked/personal_signer.rs @@ -49,7 +49,7 @@ fn miner_service() -> Arc<TestMinerService> { } fn signer_tester() -> PersonalSignerTester { - let signer = Arc::new(SignerService::new_test()); + let signer = Arc::new(SignerService::new_test(None)); let accounts = accounts_provider(); let client = blockchain_client(); let miner = miner_service(); diff --git a/rpc/src/v1/traits/ethcore.rs b/rpc/src/v1/traits/ethcore.rs index 25bb210fd..ea5f0b13d 100644 --- a/rpc/src/v1/traits/ethcore.rs +++ b/rpc/src/v1/traits/ethcore.rs @@ -117,5 +117,13 @@ build_rpc_trait! { /// Hash a file content under given URL. #[rpc(async, name = "ethcore_hashContent")] fn hash_content(&self, Ready<H256>, String); + + /// Returns current Trusted Signer port or an error if signer is disabled. + #[rpc(name = "ethcore_signerPort")] + fn signer_port(&self) -> Result<u16, Error>; + + /// Returns current Dapps Server port or an error if dapps server is disabled. + #[rpc(name = "ethcore_dappsPort")] + fn dapps_port(&self) -> Result<u16, Error>; } -} \ No newline at end of file +} diff --git a/rpc/src/v1/traits/personal.rs b/rpc/src/v1/traits/personal.rs index cf955447f..2114131d4 100644 --- a/rpc/src/v1/traits/personal.rs +++ b/rpc/src/v1/traits/personal.rs @@ -50,9 +50,6 @@ pub trait Personal: Sized + Send + Sync + 'static { /// Sends transaction and signs it in single call. The account is not unlocked in such case. fn sign_and_send_transaction(&self, _: Params) -> Result<Value, Error>; - /// Returns `true` if Trusted Signer is enabled, `false` otherwise. - fn signer_enabled(&self, _: Params) -> Result<Value, Error>; - /// Set an account's name. fn set_account_name(&self, _: Params) -> Result<Value, Error>; @@ -71,7 +68,6 @@ pub trait Personal: Sized + Send + Sync + 'static { /// Should be used to convert object to io delegate. fn to_delegate(self) -> IoDelegate<Self> { let mut delegate = IoDelegate::new(Arc::new(self)); - delegate.add_method("personal_signerEnabled", Personal::signer_enabled); delegate.add_method("personal_listAccounts", Personal::accounts); delegate.add_method("personal_newAccount", Personal::new_account); delegate.add_method("personal_newAccountFromPhrase", Personal::new_account_from_phrase); From 7e84b078dd90a506360733b694a985f44aed4a1a Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Mon, 24 Oct 2016 12:21:52 +0200 Subject: [PATCH 14/77] Adjust paths to handle CORS changes (#2816) * Adjust :8080 paths for CORS & development * No need to redirect, Node takes care of it --- js/src/dapps/registry/ui/image.js | 4 +++- js/src/dapps/tokenreg/Tokens/Token/token.js | 3 ++- js/src/environment/index.js | 6 ++++++ js/src/index.js | 6 ------ js/src/redux/providers/imagesReducer.js | 4 +++- js/src/redux/providers/status.js | 4 +++- js/src/views/Dapp/dapp.js | 7 ++----- js/src/views/Dapps/available.js | 6 ++++-- 8 files changed, 23 insertions(+), 17 deletions(-) diff --git a/js/src/dapps/registry/ui/image.js b/js/src/dapps/registry/ui/image.js index 7e7a52a88..e0101e1f2 100644 --- a/js/src/dapps/registry/ui/image.js +++ b/js/src/dapps/registry/ui/image.js @@ -16,6 +16,8 @@ import React from 'react'; +import { parityNode } from '../../../environment'; + const styles = { padding: '.5em', border: '1px solid #777' @@ -23,7 +25,7 @@ const styles = { export default (address) => ( <img - src={ `http://127.0.0.1:8080/${address}/` } + src={ `${parityNode}/${address}/` } alt={ address } style={ styles } /> diff --git a/js/src/dapps/tokenreg/Tokens/Token/token.js b/js/src/dapps/tokenreg/Tokens/Token/token.js index bee925689..18942b085 100644 --- a/js/src/dapps/tokenreg/Tokens/Token/token.js +++ b/js/src/dapps/tokenreg/Tokens/Token/token.js @@ -30,6 +30,7 @@ import styles from './token.css'; import { metaDataKeys } from '../../constants'; import { api } from '../../parity'; +import { parityNode } from '../../../../environment'; export default class Token extends Component { static propTypes = { @@ -267,7 +268,7 @@ export default class Token extends Component { </span> meta-data: </p> <div className={ styles['meta-image'] }> - <img src={ `http://127.0.0.1:8080/api/content/${imageHash}/` } /> + <img src={ `${parityNode}/api/content/${imageHash}/` } /> </div> </div>); } diff --git a/js/src/environment/index.js b/js/src/environment/index.js index d8e81f343..1dfda77aa 100644 --- a/js/src/environment/index.js +++ b/js/src/environment/index.js @@ -18,3 +18,9 @@ // import './perf-debug'; import './tests'; + +const parityNode = process.env.NODE_ENV === 'production' ? 'http://127.0.0.1:8080' : ''; + +export { + parityNode +}; diff --git a/js/src/index.js b/js/src/index.js index 08b02c2a3..24649b026 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -17,12 +17,6 @@ import 'babel-polyfill'; import 'whatwg-fetch'; -// redirect when not on 127.0.0.1:8180 -const host = `${window.location.hostname}:${window.location.port}`; -if (host === '127.0.0.1:8080' || host === 'localhost:8080') { - window.location = 'http://127.0.0.1:8180'; -} - import es6Promise from 'es6-promise'; es6Promise.polyfill(); diff --git a/js/src/redux/providers/imagesReducer.js b/js/src/redux/providers/imagesReducer.js index e3e8cbbfc..15745932d 100644 --- a/js/src/redux/providers/imagesReducer.js +++ b/js/src/redux/providers/imagesReducer.js @@ -17,6 +17,8 @@ import { handleActions } from 'redux-actions'; import { bytesToHex } from '../../api/util/format'; +import { parityNode } from '../../environment'; + const ZERO = '0x0000000000000000000000000000000000000000000000000000000000000000'; const initialState = { @@ -26,7 +28,7 @@ const initialState = { export function hashToImageUrl (hashArray) { const hash = hashArray ? bytesToHex(hashArray) : ZERO; - return hash === ZERO ? null : `http://127.0.0.1:8080/api/content/${hash.substr(2)}`; + return hash === ZERO ? null : `${parityNode}/api/content/${hash.substr(2)}`; } export default handleActions({ diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index 2e039134b..8d5874698 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -16,6 +16,8 @@ import { statusBlockNumber, statusCollection, statusLogs } from './statusActions'; +import { parityNode } from '../../environment'; + export default class Status { constructor (store, api) { this._api = api; @@ -49,7 +51,7 @@ export default class Status { setTimeout(this._pollPing, timeout); }; - fetch('/', { method: 'GET' }) + fetch(`${parityNode}/api/ping`, { method: 'GET' }) .then((response) => dispatch(!!response.ok)) .catch(() => dispatch(false)); } diff --git a/js/src/views/Dapp/dapp.js b/js/src/views/Dapp/dapp.js index b6291d2f5..6132b200a 100644 --- a/js/src/views/Dapp/dapp.js +++ b/js/src/views/Dapp/dapp.js @@ -18,10 +18,7 @@ import React, { Component, PropTypes } from 'react'; import styles from './dapp.css'; -const hostname = `${window.location.hostname}:${window.location.port}`; -const dapphost = (hostname === 'localhost:3000') || (hostname === '127.0.0.1:3000') - ? hostname - : '127.0.0.1:8080/ui'; +const dapphost = process.env.NODE_ENV === 'production' ? 'http://127.0.0.1:8080/ui' : ''; export default class Dapp extends Component { static propTypes = { @@ -31,7 +28,7 @@ export default class Dapp extends Component { render () { const { name, type } = this.props.params; const src = type === 'global' - ? `http://${dapphost}/${name}.html` + ? `${dapphost}/${name}.html` : `http://127.0.0.1:8080/${name}/`; return ( diff --git a/js/src/views/Dapps/available.js b/js/src/views/Dapps/available.js index 3ec7df8f8..c0c2e51b3 100644 --- a/js/src/views/Dapps/available.js +++ b/js/src/views/Dapps/available.js @@ -14,6 +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 { parityNode } from '../../environment'; + const builtinApps = [ { id: '0xf9f2d620c2e08f83e45555247146c62185e4ab7cf82a4b9002a265a0d020348f', @@ -72,7 +74,7 @@ const builtinApps = [ ]; export default function () { - return fetch('http://127.0.0.1:8080/api/apps') + return fetch(`${parityNode}/api/apps`) .then((response) => { return response.ok ? response.json() @@ -84,7 +86,7 @@ export default function () { }) .then((localApps) => { return builtinApps - .concat(localApps) + .concat(localApps.filter((app) => !['ui'].includes(app.id))) .sort((a, b) => a.name.localeCompare(b.name)); }); } From 66a8d534ef5b8ca2a728d2fd10f7f55a62a1e447 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Mon, 24 Oct 2016 12:28:32 +0200 Subject: [PATCH 15/77] Bump js-precompiled to 20161022-223915 UTC (#2826) --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 33581c9e8..5d18a9074 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1207,7 +1207,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#eba8fdcb29c2230868b6604dd341513b5beceba9" +source = "git+https://github.com/ethcore/js-precompiled.git#28ab89f9944d4ec8943868c4147db98d853d88e4" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] From 35925db825ed3eab119963ed5f9e45028b3c9b63 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac <ngotchac@gmail.com> Date: Mon, 24 Oct 2016 13:03:44 +0200 Subject: [PATCH 16/77] Double click on address in account detail view should select it --- js/src/views/Account/Header/header.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/views/Account/Header/header.js b/js/src/views/Account/Header/header.js index ff2828f28..2e7d5ae77 100644 --- a/js/src/views/Account/Header/header.js +++ b/js/src/views/Account/Header/header.js @@ -107,7 +107,7 @@ export default class Header extends Component { /> </IconButton> </CopyToClipboard> - { address } + <span>{ address } </span> </div> { uuidText } <div className={ styles.infoline }> From 44a560e9644a5667584493ae00443c34cb4b49fb Mon Sep 17 00:00:00 2001 From: Arkadiy Paronyan <arkady.paronyan@gmail.com> Date: Mon, 24 Oct 2016 15:09:13 +0200 Subject: [PATCH 17/77] CLI option to skip seal check when importing (#2842) --- ethcore/src/client/client.rs | 2 +- ethcore/src/client/config.rs | 2 ++ ethcore/src/client/mod.rs | 1 + ethcore/src/verification/mod.rs | 14 +++++++++++++- ethcore/src/verification/queue/kind.rs | 13 ++++++++----- ethcore/src/verification/queue/mod.rs | 8 ++++---- ethcore/src/verification/verification.rs | 10 ++++++---- parity/blockchain.rs | 6 ++++-- parity/cli/mod.rs | 2 ++ parity/cli/usage.txt | 1 + parity/configuration.rs | 7 +++++++ parity/helpers.rs | 4 +++- parity/run.rs | 2 ++ parity/snapshot.rs | 2 +- 14 files changed, 55 insertions(+), 19 deletions(-) diff --git a/ethcore/src/client/client.rs b/ethcore/src/client/client.rs index ffb44c8c2..49195d952 100644 --- a/ethcore/src/client/client.rs +++ b/ethcore/src/client/client.rs @@ -204,7 +204,7 @@ impl Client { let engine = spec.engine.clone(); - let block_queue = BlockQueue::new(config.queue.clone(), engine.clone(), message_channel.clone()); + let block_queue = BlockQueue::new(config.queue.clone(), engine.clone(), message_channel.clone(), config.verifier_type.verifying_seal()); let panic_handler = PanicHandler::new_in_arc(); panic_handler.forward_from(&block_queue); diff --git a/ethcore/src/client/config.rs b/ethcore/src/client/config.rs index b87da437f..850e5c938 100644 --- a/ethcore/src/client/config.rs +++ b/ethcore/src/client/config.rs @@ -117,6 +117,8 @@ pub struct ClientConfig { pub jump_table_size: usize, /// State pruning history size. pub history: u64, + /// Check seal valididity on block import + pub check_seal: bool, } #[cfg(test)] diff --git a/ethcore/src/client/mod.rs b/ethcore/src/client/mod.rs index 1e8aa9d72..3898ab6cd 100644 --- a/ethcore/src/client/mod.rs +++ b/ethcore/src/client/mod.rs @@ -37,6 +37,7 @@ 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 #[cfg(feature="ipc")] diff --git a/ethcore/src/verification/mod.rs b/ethcore/src/verification/mod.rs index ed9e0ec4c..239c88597 100644 --- a/ethcore/src/verification/mod.rs +++ b/ethcore/src/verification/mod.rs @@ -31,6 +31,8 @@ pub use self::queue::{BlockQueue, Config as QueueConfig, VerificationQueue, Queu pub enum VerifierType { /// Verifies block normally. Canon, + /// Verifies block normallly, but skips seal verification. + CanonNoSeal, /// Does not verify block at all. /// Used in tests. Noop, @@ -44,7 +46,17 @@ impl Default for VerifierType { pub fn new(v: VerifierType) -> Box<Verifier> { match v { - VerifierType::Canon => Box::new(CanonVerifier), + VerifierType::Canon | VerifierType::CanonNoSeal => Box::new(CanonVerifier), VerifierType::Noop => Box::new(NoopVerifier), } } + +impl VerifierType { + /// Check if seal verification is enabled for this verifier type. + pub fn verifying_seal(&self) -> bool { + match *self { + VerifierType::Canon => true, + VerifierType::Noop | VerifierType::CanonNoSeal => false, + } + } +} diff --git a/ethcore/src/verification/queue/kind.rs b/ethcore/src/verification/queue/kind.rs index b6b6c5cf6..17b997490 100644 --- a/ethcore/src/verification/queue/kind.rs +++ b/ethcore/src/verification/queue/kind.rs @@ -57,7 +57,7 @@ pub trait Kind: 'static + Sized + Send + Sync { fn create(input: Self::Input, engine: &Engine) -> Result<Self::Unverified, Error>; /// Attempt to verify the `Unverified` item using the given engine. - fn verify(unverified: Self::Unverified, engine: &Engine) -> Result<Self::Verified, Error>; + fn verify(unverified: Self::Unverified, engine: &Engine, check_seal: bool) -> Result<Self::Verified, Error>; } /// The blocks verification module. @@ -89,9 +89,9 @@ pub mod blocks { } } - fn verify(un: Self::Unverified, engine: &Engine) -> Result<Self::Verified, Error> { + fn verify(un: Self::Unverified, engine: &Engine, check_seal: bool) -> Result<Self::Verified, Error> { let hash = un.hash(); - match verify_block_unordered(un.header, un.bytes, engine) { + match verify_block_unordered(un.header, un.bytes, engine, check_seal) { Ok(verified) => Ok(verified), Err(e) => { warn!(target: "client", "Stage 2 block verification failed for {}: {:?}", hash, e); @@ -176,8 +176,11 @@ pub mod headers { verify_header_params(&input, engine).map(|_| input) } - fn verify(unverified: Self::Unverified, engine: &Engine) -> Result<Self::Verified, Error> { - engine.verify_block_unordered(&unverified, None).map(|_| unverified) + fn verify(unverified: Self::Unverified, engine: &Engine, check_seal: bool) -> Result<Self::Verified, Error> { + match check_seal { + true => engine.verify_block_unordered(&unverified, None).map(|_| unverified), + false => Ok(unverified), + } } } } diff --git a/ethcore/src/verification/queue/mod.rs b/ethcore/src/verification/queue/mod.rs index cf6ca3f53..c4ee53c23 100644 --- a/ethcore/src/verification/queue/mod.rs +++ b/ethcore/src/verification/queue/mod.rs @@ -159,7 +159,7 @@ struct Verification<K: Kind> { impl<K: Kind> VerificationQueue<K> { /// Creates a new queue instance. - pub fn new(config: Config, engine: Arc<Engine>, message_channel: IoChannel<ClientIoMessage>) -> Self { + pub fn new(config: Config, engine: Arc<Engine>, message_channel: IoChannel<ClientIoMessage>, check_seal: bool) -> Self { let verification = Arc::new(Verification { unverified: Mutex::new(VecDeque::new()), verifying: Mutex::new(VecDeque::new()), @@ -198,7 +198,7 @@ impl<K: Kind> VerificationQueue<K> { .name(format!("Verifier #{}", i)) .spawn(move || { panic_handler.catch_panic(move || { - VerificationQueue::verify(verification, engine, more_to_verify, ready_signal, deleting, empty) + VerificationQueue::verify(verification, engine, more_to_verify, ready_signal, deleting, empty, check_seal) }).unwrap() }) .expect("Error starting block verification thread") @@ -219,7 +219,7 @@ impl<K: Kind> VerificationQueue<K> { } } - fn verify(verification: Arc<Verification<K>>, engine: Arc<Engine>, wait: Arc<SCondvar>, ready: Arc<QueueSignal>, deleting: Arc<AtomicBool>, empty: Arc<SCondvar>) { + fn verify(verification: Arc<Verification<K>>, engine: Arc<Engine>, wait: Arc<SCondvar>, ready: Arc<QueueSignal>, deleting: Arc<AtomicBool>, empty: Arc<SCondvar>, check_seal: bool) { while !deleting.load(AtomicOrdering::Acquire) { { let mut more_to_verify = verification.more_to_verify.lock().unwrap(); @@ -253,7 +253,7 @@ impl<K: Kind> VerificationQueue<K> { }; let hash = item.hash(); - let is_ready = match K::verify(item, &*engine) { + let is_ready = match K::verify(item, &*engine, check_seal) { Ok(verified) => { let mut verifying = verification.verifying.lock(); let mut idx = None; diff --git a/ethcore/src/verification/verification.rs b/ethcore/src/verification/verification.rs index 2d297499d..eefed1261 100644 --- a/ethcore/src/verification/verification.rs +++ b/ethcore/src/verification/verification.rs @@ -66,10 +66,12 @@ pub fn verify_block_basic(header: &Header, bytes: &[u8], engine: &Engine) -> Res /// Phase 2 verification. Perform costly checks such as transaction signatures and block nonce for ethash. /// Still operates on a individual block /// Returns a `PreverifiedBlock` structure populated with transactions -pub fn verify_block_unordered(header: Header, bytes: Bytes, engine: &Engine) -> Result<PreverifiedBlock, Error> { - try!(engine.verify_block_unordered(&header, Some(&bytes))); - for u in try!(UntrustedRlp::new(&bytes).at(2)).iter().map(|rlp| rlp.as_val::<Header>()) { - try!(engine.verify_block_unordered(&try!(u), None)); +pub fn verify_block_unordered(header: Header, bytes: Bytes, engine: &Engine, check_seal: bool) -> Result<PreverifiedBlock, Error> { + if check_seal { + try!(engine.verify_block_unordered(&header, Some(&bytes))); + for u in try!(UntrustedRlp::new(&bytes).at(2)).iter().map(|rlp| rlp.as_val::<Header>()) { + try!(engine.verify_block_unordered(&try!(u), None)); + } } // Verify transactions. let mut transactions = Vec::new(); diff --git a/parity/blockchain.rs b/parity/blockchain.rs index fbf25e1cf..2e0fb4233 100644 --- a/parity/blockchain.rs +++ b/parity/blockchain.rs @@ -84,6 +84,7 @@ pub struct ImportBlockchain { pub tracing: Switch, pub fat_db: Switch, pub vm_type: VMType, + pub check_seal: bool, } #[derive(Debug, PartialEq)] @@ -103,6 +104,7 @@ pub struct ExportBlockchain { pub tracing: Switch, pub from_block: BlockID, pub to_block: BlockID, + pub check_seal: bool, } pub fn execute(cmd: BlockchainCmd) -> Result<String, String> { @@ -158,7 +160,7 @@ fn execute_import(cmd: ImportBlockchain) -> Result<String, String> { try!(execute_upgrades(&db_dirs, algorithm, cmd.compaction.compaction_profile(db_dirs.fork_path().as_path()))); // prepare client config - let client_config = to_client_config(&cmd.cache_config, cmd.mode, tracing, fat_db, cmd.compaction, cmd.wal, cmd.vm_type, "".into(), algorithm, cmd.pruning_history); + let client_config = to_client_config(&cmd.cache_config, cmd.mode, tracing, fat_db, cmd.compaction, cmd.wal, cmd.vm_type, "".into(), algorithm, cmd.pruning_history, cmd.check_seal); // build client let service = try!(ClientService::start( @@ -309,7 +311,7 @@ fn execute_export(cmd: ExportBlockchain) -> Result<String, String> { try!(execute_upgrades(&db_dirs, algorithm, cmd.compaction.compaction_profile(db_dirs.fork_path().as_path()))); // prepare client config - let client_config = to_client_config(&cmd.cache_config, cmd.mode, tracing, fat_db, cmd.compaction, cmd.wal, VMType::default(), "".into(), algorithm, cmd.pruning_history); + let client_config = to_client_config(&cmd.cache_config, cmd.mode, tracing, fat_db, cmd.compaction, cmd.wal, VMType::default(), "".into(), algorithm, cmd.pruning_history, cmd.check_seal); let service = try!(ClientService::start( client_config, diff --git a/parity/cli/mod.rs b/parity/cli/mod.rs index a8e1cdba7..9c4a45e12 100644 --- a/parity/cli/mod.rs +++ b/parity/cli/mod.rs @@ -234,6 +234,7 @@ usage! { flag_from: String = "1", or |_| None, flag_to: String = "latest", or |_| None, flag_format: Option<String> = None, or |_| None, + flag_no_seal_check: bool = false, or |_| None, // -- Snapshot Optons flag_at: String = "latest", or |_| None, @@ -561,6 +562,7 @@ mod tests { flag_from: "1".into(), flag_to: "latest".into(), flag_format: None, + flag_no_seal_check: false, // -- Snapshot Optons flag_at: "latest".into(), diff --git a/parity/cli/usage.txt b/parity/cli/usage.txt index 8bf99c5e7..fe6203842 100644 --- a/parity/cli/usage.txt +++ b/parity/cli/usage.txt @@ -247,6 +247,7 @@ Import/Export Options: --format FORMAT For import/export in given format. FORMAT must be one of 'hex' and 'binary'. (default: {flag_format:?} = Import: auto, Export: binary) + --no-seal-check Skip block seal check. (default: {flag_no_seal_check}) Snapshot Options: --at BLOCK Take a snapshot at the given block, which may be an diff --git a/parity/configuration.rs b/parity/configuration.rs index ddfb03669..060679b0e 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -154,6 +154,7 @@ impl Configuration { tracing: tracing, fat_db: fat_db, vm_type: vm_type, + check_seal: !self.args.flag_no_seal_check, }; Cmd::Blockchain(BlockchainCmd::Import(import_cmd)) } else if self.args.cmd_export { @@ -173,6 +174,7 @@ impl Configuration { fat_db: fat_db, from_block: try!(to_block_id(&self.args.flag_from)), to_block: try!(to_block_id(&self.args.flag_to)), + check_seal: !self.args.flag_no_seal_check, }; Cmd::Blockchain(BlockchainCmd::Export(export_cmd)) } else if self.args.cmd_snapshot { @@ -251,6 +253,7 @@ impl Configuration { name: self.args.flag_identity, custom_bootnodes: self.args.flag_bootnodes.is_some(), no_periodic_snapshot: self.args.flag_no_periodic_snapshot, + check_seal: !self.args.flag_no_seal_check, }; Cmd::Run(run_cmd) }; @@ -738,6 +741,7 @@ mod tests { tracing: Default::default(), fat_db: Default::default(), vm_type: VMType::Interpreter, + check_seal: true, }))); } @@ -761,6 +765,7 @@ mod tests { fat_db: Default::default(), from_block: BlockID::Number(1), to_block: BlockID::Latest, + check_seal: true, }))); } @@ -784,6 +789,7 @@ mod tests { fat_db: Default::default(), from_block: BlockID::Number(1), to_block: BlockID::Latest, + check_seal: true, }))); } @@ -832,6 +838,7 @@ mod tests { custom_bootnodes: false, fat_db: Default::default(), no_periodic_snapshot: false, + check_seal: true, })); } diff --git a/parity/helpers.rs b/parity/helpers.rs index af947329b..a965314f5 100644 --- a/parity/helpers.rs +++ b/parity/helpers.rs @@ -21,7 +21,7 @@ use std::path::Path; use std::fs::File; use util::{clean_0x, U256, Uint, Address, path, CompactionProfile}; use util::journaldb::Algorithm; -use ethcore::client::{Mode, BlockID, VMType, DatabaseCompactionProfile, ClientConfig}; +use ethcore::client::{Mode, BlockID, VMType, DatabaseCompactionProfile, ClientConfig, VerifierType}; use ethcore::miner::{PendingSet, GasLimit, PrioritizationStrategy}; use cache::CacheConfig; use dir::DatabaseDirectories; @@ -215,6 +215,7 @@ pub fn to_client_config( name: String, pruning: Algorithm, pruning_history: u64, + check_seal: bool, ) -> ClientConfig { let mut client_config = ClientConfig::default(); @@ -247,6 +248,7 @@ pub fn to_client_config( client_config.db_wal = wal; client_config.vm_type = vm_type; client_config.name = name; + client_config.verifier_type = if check_seal { VerifierType::Canon } else { VerifierType::CanonNoSeal }; client_config } diff --git a/parity/run.rs b/parity/run.rs index 46cc543f2..7f35f3514 100644 --- a/parity/run.rs +++ b/parity/run.rs @@ -90,6 +90,7 @@ pub struct RunCmd { pub name: String, pub custom_bootnodes: bool, pub no_periodic_snapshot: bool, + pub check_seal: bool, } pub fn execute(cmd: RunCmd) -> Result<(), String> { @@ -197,6 +198,7 @@ pub fn execute(cmd: RunCmd) -> Result<(), String> { cmd.name, algorithm, cmd.pruning_history, + cmd.check_seal, ); // set up bootnodes diff --git a/parity/snapshot.rs b/parity/snapshot.rs index dc146d8fe..e5c3c672c 100644 --- a/parity/snapshot.rs +++ b/parity/snapshot.rs @@ -163,7 +163,7 @@ impl SnapshotCommand { try!(execute_upgrades(&db_dirs, algorithm, self.compaction.compaction_profile(db_dirs.fork_path().as_path()))); // prepare client config - let client_config = to_client_config(&self.cache_config, self.mode, tracing, fat_db, self.compaction, self.wal, VMType::default(), "".into(), algorithm, self.pruning_history); + let client_config = to_client_config(&self.cache_config, self.mode, tracing, fat_db, self.compaction, self.wal, VMType::default(), "".into(), algorithm, self.pruning_history, true); let service = try!(ClientService::start( client_config, From e0207b594b0ebcdc726e8356a54cb77ccd349922 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Mon, 24 Oct 2016 15:09:33 +0200 Subject: [PATCH 18/77] Add ethcore_[dapps|signer]Port APIs (#2821) * Add ethcore_[dapps|signer]Port APIs * typo --- js/src/api/rpc/ethcore/ethcore.js | 12 ++++++++++++ js/src/jsonrpc/interfaces/ethcore.js | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/js/src/api/rpc/ethcore/ethcore.js b/js/src/api/rpc/ethcore/ethcore.js index 1ccc95bba..b9dec3b69 100644 --- a/js/src/api/rpc/ethcore/ethcore.js +++ b/js/src/api/rpc/ethcore/ethcore.js @@ -32,6 +32,12 @@ export default class Ethcore { .execute('ethcore_addReservedPeer', encode); } + dappsPort () { + return this._transport + .execute('ethcore_dappsPort') + .then(outNumber); + } + defaultExtraData () { return this._transport .execute('ethcore_defaultExtraData'); @@ -154,6 +160,12 @@ export default class Ethcore { .execute('ethcore_setTransactionsLimit', inNumber16(quantity)); } + signerPort () { + return this._transport + .execute('ethcore_signerPort') + .then(outNumber); + } + transactionsLimit () { return this._transport .execute('ethcore_transactionsLimit') diff --git a/js/src/jsonrpc/interfaces/ethcore.js b/js/src/jsonrpc/interfaces/ethcore.js index 5b2910618..4ebff1952 100644 --- a/js/src/jsonrpc/interfaces/ethcore.js +++ b/js/src/jsonrpc/interfaces/ethcore.js @@ -40,6 +40,15 @@ export default { } }, + dappsPort: { + desc: 'Returns the port the dapps are running on, error if not enabled', + params: [], + returns: { + type: Quantity, + desc: 'The port number' + } + }, + defaultExtraData: { desc: 'Returns the default extra data', params: [], @@ -294,6 +303,15 @@ export default { } }, + signerPort: { + desc: 'Returns the port the signer is running on, error if not enabled', + params: [], + returns: { + type: Quantity, + desc: 'The port number' + } + }, + transactionsLimit: { desc: 'Changes limit for transactions in queue.', params: [], From a293493f930289606a5cd3aa80635e5a5e93627a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= <tomusdrw@users.noreply.github.com> Date: Mon, 24 Oct 2016 15:09:47 +0200 Subject: [PATCH 19/77] Clear cached content (#2833) --- dapps/src/apps/cache.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dapps/src/apps/cache.rs b/dapps/src/apps/cache.rs index c9ca12db9..9d1642fb0 100644 --- a/dapps/src/apps/cache.rs +++ b/dapps/src/apps/cache.rs @@ -66,10 +66,10 @@ impl ContentCache { }, ContentStatus::Ready(ref endpoint) => { trace!(target: "dapps", "Removing {} because of limit.", entry.0); - // Remove path - let res = fs::remove_dir_all(&endpoint.path()); + // Remove path (dir or file) + let res = fs::remove_dir_all(&endpoint.path()).or_else(|_| fs::remove_file(&endpoint.path())); if let Err(e) = res { - warn!(target: "dapps", "Unable to remove dapp: {:?}", e); + warn!(target: "dapps", "Unable to remove dapp/content from cache: {:?}", e); } } } From 4ea67ff91d201fcad8d8958f90a73e51cce2248d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= <tomusdrw@users.noreply.github.com> Date: Mon, 24 Oct 2016 15:10:13 +0200 Subject: [PATCH 20/77] Disable personal APIs by default (#2834) --- parity/cli/config.full.toml | 4 ++-- parity/cli/mod.rs | 8 ++++---- parity/rpc_apis.rs | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/parity/cli/config.full.toml b/parity/cli/config.full.toml index 6d9c84fd4..b82762684 100644 --- a/parity/cli/config.full.toml +++ b/parity/cli/config.full.toml @@ -38,13 +38,13 @@ disable = false port = 8545 interface = "local" cors = "null" -apis = ["web3", "eth", "net", "personal", "ethcore", "traces", "rpc"] +apis = ["web3", "eth", "net", "ethcore", "traces", "rpc"] hosts = ["none"] [ipc] disable = false path = "$HOME/.parity/jsonrpc.ipc" -apis = ["web3", "eth", "net", "personal", "ethcore", "traces", "rpc"] +apis = ["web3", "eth", "net", "ethcore", "traces", "rpc"] [dapps] disable = false diff --git a/parity/cli/mod.rs b/parity/cli/mod.rs index 9c4a45e12..d4558a50d 100644 --- a/parity/cli/mod.rs +++ b/parity/cli/mod.rs @@ -139,7 +139,7 @@ usage! { or |c: &Config| otry!(c.rpc).interface.clone(), flag_jsonrpc_cors: Option<String> = None, or |c: &Config| otry!(c.rpc).cors.clone().map(Some), - flag_jsonrpc_apis: String = "web3,eth,net,ethcore,personal,traces,rpc", + flag_jsonrpc_apis: String = "web3,eth,net,ethcore,traces,rpc", or |c: &Config| otry!(c.rpc).apis.clone().map(|vec| vec.join(",")), flag_jsonrpc_hosts: String = "none", or |c: &Config| otry!(c.rpc).hosts.clone().map(|vec| vec.join(",")), @@ -149,7 +149,7 @@ usage! { or |c: &Config| otry!(c.ipc).disable.clone(), flag_ipc_path: String = "$HOME/.parity/jsonrpc.ipc", or |c: &Config| otry!(c.ipc).path.clone(), - flag_ipc_apis: String = "web3,eth,net,ethcore,personal,traces,rpc", + flag_ipc_apis: String = "web3,eth,net,ethcore,traces,rpc", or |c: &Config| otry!(c.ipc).apis.clone().map(|vec| vec.join(",")), // DAPPS @@ -508,13 +508,13 @@ mod tests { flag_jsonrpc_port: 8545u16, flag_jsonrpc_interface: "local".into(), flag_jsonrpc_cors: Some("null".into()), - flag_jsonrpc_apis: "web3,eth,net,personal,ethcore,traces,rpc".into(), + flag_jsonrpc_apis: "web3,eth,net,ethcore,traces,rpc".into(), flag_jsonrpc_hosts: "none".into(), // IPC flag_no_ipc: false, flag_ipc_path: "$HOME/.parity/jsonrpc.ipc".into(), - flag_ipc_apis: "web3,eth,net,personal,ethcore,traces,rpc".into(), + flag_ipc_apis: "web3,eth,net,ethcore,traces,rpc".into(), // DAPPS flag_no_dapps: false, diff --git a/parity/rpc_apis.rs b/parity/rpc_apis.rs index 825e2db3a..f6ccf16a3 100644 --- a/parity/rpc_apis.rs +++ b/parity/rpc_apis.rs @@ -131,10 +131,10 @@ impl ApiSet { match *self { ApiSet::List(ref apis) => apis.clone(), ApiSet::UnsafeContext => { - vec![Api::Web3, Api::Net, Api::Eth, Api::Personal, Api::Ethcore, Api::Traces, Api::Rpc] + vec![Api::Web3, Api::Net, Api::Eth, Api::Ethcore, Api::Traces, Api::Rpc] .into_iter().collect() }, - _ => { + ApiSet::SafeContext => { vec![Api::Web3, Api::Net, Api::Eth, Api::Personal, Api::Signer, Api::Ethcore, Api::EthcoreSet, Api::Traces, Api::Rpc] .into_iter().collect() }, @@ -245,7 +245,7 @@ mod test { #[test] fn test_api_set_unsafe_context() { - let expected = vec![Api::Web3, Api::Net, Api::Eth, Api::Personal, Api::Ethcore, Api::Traces, Api::Rpc] + let expected = vec![Api::Web3, Api::Net, Api::Eth, Api::Ethcore, Api::Traces, Api::Rpc] .into_iter().collect(); assert_eq!(ApiSet::UnsafeContext.list_apis(), expected); } From f4203a25711e78163a1b73081a731f51590a61b0 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac <ngotchac@gmail.com> Date: Mon, 24 Oct 2016 15:10:27 +0200 Subject: [PATCH 21/77] Fixes currency symbol font size in Shapeshift modal (#2840) * Fix case error in Dapps import * Fixed big input in modal and currency symbols size (#2718) * `em` instead of `rem` --- js/src/modals/Shapeshift/OptionsStep/optionsStep.css | 2 +- js/src/modals/Shapeshift/Value/value.css | 6 ++++++ js/src/modals/Shapeshift/Value/value.js | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/js/src/modals/Shapeshift/OptionsStep/optionsStep.css b/js/src/modals/Shapeshift/OptionsStep/optionsStep.css index 23cc0f9b7..3433e3d7a 100644 --- a/js/src/modals/Shapeshift/OptionsStep/optionsStep.css +++ b/js/src/modals/Shapeshift/OptionsStep/optionsStep.css @@ -18,7 +18,7 @@ } .accept { - padding: 1.5em 0; + margin: 1.5em 0; } .coinselector { diff --git a/js/src/modals/Shapeshift/Value/value.css b/js/src/modals/Shapeshift/Value/value.css index 746b5d168..3f4a40fd4 100644 --- a/js/src/modals/Shapeshift/Value/value.css +++ b/js/src/modals/Shapeshift/Value/value.css @@ -22,3 +22,9 @@ .amount { display: inline-block; } + +.symbol { + font-variant: small-caps; + margin-left: 0.1rem; + font-size: 0.8em; +} diff --git a/js/src/modals/Shapeshift/Value/value.js b/js/src/modals/Shapeshift/Value/value.js index be8c44711..e3c3050fb 100644 --- a/js/src/modals/Shapeshift/Value/value.js +++ b/js/src/modals/Shapeshift/Value/value.js @@ -38,7 +38,8 @@ export default class Value extends Component { return ( <div className={ styles.body }> - <span>{ value }</span><small>{ symbol || 'ETH' }</small> + <span>{ value } </span> + <span className={ styles.symbol }>{ symbol || 'ETH' }</span> </div> ); } From ff347da8d30ca8973205659d69eff5427004de7f Mon Sep 17 00:00:00 2001 From: Jannis Redmann <mail@jannisr.de> Date: Mon, 24 Oct 2016 15:11:32 +0200 Subject: [PATCH 22/77] fix node log being reversed (#2839) --- js/src/views/Status/components/Debug/Debug.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/views/Status/components/Debug/Debug.js b/js/src/views/Status/components/Debug/Debug.js index 29cc267b9..5be4d594f 100644 --- a/js/src/views/Status/components/Debug/Debug.js +++ b/js/src/views/Status/components/Debug/Debug.js @@ -74,7 +74,7 @@ export default class Debug extends Component { return ( <pre className={ styles.logs }> - { devLogs.join('\n') } + { devLogs.reverse().join('\n') } </pre> ); } From 9ec091e0cffbb36e1a5e81c1ef3d3ab767a5533f Mon Sep 17 00:00:00 2001 From: Arkadiy Paronyan <arkady.paronyan@gmail.com> Date: Mon, 24 Oct 2016 16:24:35 +0200 Subject: [PATCH 23/77] Move snapshot sync to a subprotocol (#2820) --- sync/src/api.rs | 27 +++++++++++++++++++++------ sync/src/chain.rs | 32 +++++++++++++++++++++----------- sync/src/sync_io.rs | 12 +++++++++--- sync/src/tests/helpers.rs | 7 ++++++- util/network/src/host.rs | 12 +++++++----- util/network/src/lib.rs | 8 ++------ util/network/src/service.rs | 3 ++- util/network/src/session.rs | 23 ++++++++++++++++++++++- util/network/src/tests.rs | 4 ++-- 9 files changed, 92 insertions(+), 36 deletions(-) diff --git a/sync/src/api.rs b/sync/src/api.rs index 1ea0a5bbb..d0d734024 100644 --- a/sync/src/api.rs +++ b/sync/src/api.rs @@ -17,7 +17,7 @@ use std::sync::Arc; use std::collections::HashMap; use util::Bytes; -use network::{NetworkProtocolHandler, NetworkService, NetworkContext, PeerId, +use network::{NetworkProtocolHandler, NetworkService, NetworkContext, PeerId, ProtocolId, NetworkConfiguration as BasicNetworkConfiguration, NonReservedPeerMode, NetworkError}; use util::{U256, H256}; use io::{TimerToken}; @@ -30,6 +30,9 @@ use std::net::{SocketAddr, AddrParseError}; use ipc::{BinaryConvertable, BinaryConvertError, IpcConfig}; use std::str::FromStr; use parking_lot::RwLock; +use chain::{ETH_PACKET_COUNT, SNAPSHOT_SYNC_PACKET_COUNT}; + +pub const WARP_SYNC_PROTOCOL_ID: ProtocolId = *b"bam"; /// Sync configuration #[derive(Debug, Clone, Copy)] @@ -78,7 +81,7 @@ pub struct PeerInfo { /// Node client ID pub client_version: String, /// Capabilities - pub capabilities: Vec<String>, + pub capabilities: Vec<String>, /// Remote endpoint address pub remote_address: String, /// Local endpoint address @@ -150,7 +153,9 @@ struct SyncProtocolHandler { impl NetworkProtocolHandler for SyncProtocolHandler { fn initialize(&self, io: &NetworkContext) { - io.register_timer(0, 1000).expect("Error registering sync timer"); + if io.subprotocol_name() != WARP_SYNC_PROTOCOL_ID { + io.register_timer(0, 1000).expect("Error registering sync timer"); + } } fn read(&self, io: &NetworkContext, peer: &PeerId, packet_id: u8, data: &[u8]) { @@ -158,11 +163,18 @@ impl NetworkProtocolHandler for SyncProtocolHandler { } fn connected(&self, io: &NetworkContext, peer: &PeerId) { - self.sync.write().on_peer_connected(&mut NetSyncIo::new(io, &*self.chain, &*self.snapshot_service, &self.overlay), *peer); + // If warp protocol is supported only allow warp handshake + let warp_protocol = io.protocol_version(WARP_SYNC_PROTOCOL_ID, *peer).unwrap_or(0) != 0; + let warp_context = io.subprotocol_name() == WARP_SYNC_PROTOCOL_ID; + if warp_protocol == warp_context { + self.sync.write().on_peer_connected(&mut NetSyncIo::new(io, &*self.chain, &*self.snapshot_service, &self.overlay), *peer); + } } fn disconnected(&self, io: &NetworkContext, peer: &PeerId) { - self.sync.write().on_peer_aborting(&mut NetSyncIo::new(io, &*self.chain, &*self.snapshot_service, &self.overlay), *peer); + if io.subprotocol_name() != WARP_SYNC_PROTOCOL_ID { + self.sync.write().on_peer_aborting(&mut NetSyncIo::new(io, &*self.chain, &*self.snapshot_service, &self.overlay), *peer); + } } fn timeout(&self, io: &NetworkContext, _timer: TimerToken) { @@ -195,8 +207,11 @@ impl ChainNotify for EthSync { fn start(&self) { self.network.start().unwrap_or_else(|e| warn!("Error starting network: {:?}", e)); - self.network.register_protocol(self.handler.clone(), self.subprotocol_name, &[62u8, 63u8, 64u8]) + self.network.register_protocol(self.handler.clone(), self.subprotocol_name, ETH_PACKET_COUNT, &[62u8, 63u8]) .unwrap_or_else(|e| warn!("Error registering ethereum protocol: {:?}", e)); + // register the warp sync subprotocol + self.network.register_protocol(self.handler.clone(), WARP_SYNC_PROTOCOL_ID, SNAPSHOT_SYNC_PACKET_COUNT, &[1u8]) + .unwrap_or_else(|e| warn!("Error registering snapshot sync protocol: {:?}", e)); } fn stop(&self) { diff --git a/sync/src/chain.rs b/sync/src/chain.rs index 181d8dda3..850dff228 100644 --- a/sync/src/chain.rs +++ b/sync/src/chain.rs @@ -101,14 +101,14 @@ use super::SyncConfig; use block_sync::{BlockDownloader, BlockRequest, BlockDownloaderImportError as DownloaderImportError}; use snapshot::{Snapshot, ChunkType}; use rand::{thread_rng, Rng}; -use api::PeerInfo as PeerInfoDigest; +use api::{PeerInfo as PeerInfoDigest, WARP_SYNC_PROTOCOL_ID}; known_heap_size!(0, PeerInfo); type PacketDecodeError = DecoderError; const PROTOCOL_VERSION_63: u8 = 63; -const PROTOCOL_VERSION_64: u8 = 64; +const PROTOCOL_VERSION_1: u8 = 1; const MAX_BODIES_TO_SEND: usize = 256; const MAX_HEADERS_TO_SEND: usize = 512; const MAX_NODE_DATA_TO_SEND: usize = 1024; @@ -137,11 +137,16 @@ const GET_NODE_DATA_PACKET: u8 = 0x0d; const NODE_DATA_PACKET: u8 = 0x0e; const GET_RECEIPTS_PACKET: u8 = 0x0f; const RECEIPTS_PACKET: u8 = 0x10; + +pub const ETH_PACKET_COUNT: u8 = 0x11; + const GET_SNAPSHOT_MANIFEST_PACKET: u8 = 0x11; const SNAPSHOT_MANIFEST_PACKET: u8 = 0x12; const GET_SNAPSHOT_DATA_PACKET: u8 = 0x13; const SNAPSHOT_DATA_PACKET: u8 = 0x14; +pub const SNAPSHOT_SYNC_PACKET_COUNT: u8 = 0x15; + const HEADERS_TIMEOUT_SEC: f64 = 15f64; const BODIES_TIMEOUT_SEC: f64 = 10f64; const RECEIPTS_TIMEOUT_SEC: f64 = 10f64; @@ -354,7 +359,7 @@ impl ChainSync { let last_imported_number = self.new_blocks.last_imported_block_number(); SyncStatus { state: self.state.clone(), - protocol_version: if self.state == SyncState::SnapshotData { PROTOCOL_VERSION_64 } else { PROTOCOL_VERSION_63 }, + protocol_version: PROTOCOL_VERSION_63, network_id: self.network_id, start_block_number: self.starting_block, last_imported_block_number: Some(last_imported_number), @@ -471,6 +476,7 @@ impl ChainSync { /// Called by peer to report status fn on_peer_status(&mut self, io: &mut SyncIo, peer_id: PeerId, r: &UntrustedRlp) -> Result<(), PacketDecodeError> { let protocol_version: u8 = try!(r.val_at(0)); + let warp_protocol = io.protocol_version(&WARP_SYNC_PROTOCOL_ID, peer_id) != 0; let peer = PeerInfo { protocol_version: protocol_version, network_id: try!(r.val_at(1)), @@ -485,8 +491,8 @@ impl ChainSync { expired: false, confirmation: if self.fork_block.is_none() { ForkConfirmation::Confirmed } else { ForkConfirmation::Unconfirmed }, asking_snapshot_data: None, - snapshot_hash: if protocol_version == PROTOCOL_VERSION_64 { Some(try!(r.val_at(5))) } else { None }, - snapshot_number: if protocol_version == PROTOCOL_VERSION_64 { Some(try!(r.val_at(6))) } else { None }, + snapshot_hash: if warp_protocol { Some(try!(r.val_at(5))) } else { None }, + snapshot_number: if warp_protocol { Some(try!(r.val_at(6))) } else { None }, block_set: None, }; @@ -511,7 +517,7 @@ impl ChainSync { trace!(target: "sync", "Peer {} network id mismatch (ours: {}, theirs: {})", peer_id, self.network_id, peer.network_id); return Ok(()); } - if peer.protocol_version != PROTOCOL_VERSION_64 && peer.protocol_version != PROTOCOL_VERSION_63 { + if (warp_protocol && peer.protocol_version != PROTOCOL_VERSION_1) || (!warp_protocol && peer.protocol_version != PROTOCOL_VERSION_63) { io.disable_peer(peer_id); trace!(target: "sync", "Peer {} unsupported eth protocol ({})", peer_id, peer.protocol_version); return Ok(()); @@ -1291,17 +1297,17 @@ impl ChainSync { /// Send Status message fn send_status(&mut self, io: &mut SyncIo, peer: PeerId) -> Result<(), NetworkError> { - let protocol = io.eth_protocol_version(peer); + let warp_protocol = io.protocol_version(&WARP_SYNC_PROTOCOL_ID, peer) != 0; + let protocol = if warp_protocol { PROTOCOL_VERSION_1 } else { PROTOCOL_VERSION_63 }; trace!(target: "sync", "Sending status to {}, protocol version {}", peer, protocol); - let pv64 = protocol >= PROTOCOL_VERSION_64; - let mut packet = RlpStream::new_list(if pv64 { 7 } else { 5 }); + let mut packet = RlpStream::new_list(if warp_protocol { 7 } else { 5 }); let chain = io.chain().chain_info(); packet.append(&(protocol as u32)); packet.append(&self.network_id); packet.append(&chain.total_difficulty); packet.append(&chain.best_block_hash); packet.append(&chain.genesis_hash); - if pv64 { + if warp_protocol { let manifest = io.snapshot_service().manifest(); let block_number = manifest.as_ref().map_or(0, |m| m.block_number); let manifest_hash = manifest.map_or(H256::new(), |m| m.into_rlp().sha3()); @@ -1354,6 +1360,7 @@ impl ChainSync { let mut data = Bytes::new(); let inc = (skip + 1) as BlockNumber; let overlay = io.chain_overlay().read(); + while number <= last && count < max_count { if let Some(hdr) = overlay.get(&number) { trace!(target: "sync", "{}: Returning cached fork header", peer_id); @@ -1362,6 +1369,9 @@ impl ChainSync { } else if let Some(mut hdr) = io.chain().block_header(BlockID::Number(number)) { data.append(&mut hdr); count += 1; + } else { + // No required block. + break; } if reverse { if number <= inc || number == 0 { @@ -1471,7 +1481,7 @@ impl ChainSync { Ok(Some((SNAPSHOT_MANIFEST_PACKET, rlp))) } - /// Respond to GetSnapshotManifest request + /// Respond to GetSnapshotData request fn return_snapshot_data(io: &SyncIo, r: &UntrustedRlp, peer_id: PeerId) -> RlpResponseResult { let hash: H256 = try!(r.val_at(0)); trace!(target: "sync", "{} -> GetSnapshotData {:?}", peer_id, hash); diff --git a/sync/src/sync_io.rs b/sync/src/sync_io.rs index 24a73437b..25d235c60 100644 --- a/sync/src/sync_io.rs +++ b/sync/src/sync_io.rs @@ -15,7 +15,7 @@ // along with Parity. If not, see <http://www.gnu.org/licenses/>. use std::collections::HashMap; -use network::{NetworkContext, PeerId, PacketId, NetworkError, SessionInfo}; +use network::{NetworkContext, PeerId, PacketId, NetworkError, SessionInfo, ProtocolId}; use util::Bytes; use ethcore::client::BlockChainClient; use ethcore::header::BlockNumber; @@ -44,8 +44,10 @@ pub trait SyncIo { } /// Returns information on p2p session fn peer_session_info(&self, peer_id: PeerId) -> Option<SessionInfo>; - /// Maximum mutuallt supported ETH protocol version + /// Maximum mutually supported ETH protocol version fn eth_protocol_version(&self, peer_id: PeerId) -> u8; + /// Maximum mutually supported version of a gien protocol. + fn protocol_version(&self, protocol: &ProtocolId, peer_id: PeerId) -> u8; /// Returns if the chain block queue empty fn is_chain_queue_empty(&self) -> bool { self.chain().queue_info().is_empty() @@ -117,7 +119,11 @@ impl<'s, 'h> SyncIo for NetSyncIo<'s, 'h> { } fn eth_protocol_version(&self, peer_id: PeerId) -> u8 { - self.network.protocol_version(peer_id, self.network.subprotocol_name()).unwrap_or(0) + self.network.protocol_version(self.network.subprotocol_name(), peer_id).unwrap_or(0) + } + + fn protocol_version(&self, protocol: &ProtocolId, peer_id: PeerId) -> u8 { + self.network.protocol_version(*protocol, peer_id).unwrap_or(0) } } diff --git a/sync/src/tests/helpers.rs b/sync/src/tests/helpers.rs index 7fb2319c7..801db234d 100644 --- a/sync/src/tests/helpers.rs +++ b/sync/src/tests/helpers.rs @@ -21,6 +21,7 @@ use ethcore::client::{TestBlockChainClient, BlockChainClient}; use ethcore::header::BlockNumber; use ethcore::snapshot::SnapshotService; use sync_io::SyncIo; +use api::WARP_SYNC_PROTOCOL_ID; use chain::ChainSync; use ::SyncConfig; @@ -90,7 +91,11 @@ impl<'p> SyncIo for TestIo<'p> { } fn eth_protocol_version(&self, _peer: PeerId) -> u8 { - 64 + 63 + } + + fn protocol_version(&self, protocol: &ProtocolId, peer_id: PeerId) -> u8 { + if protocol == &WARP_SYNC_PROTOCOL_ID { 1 } else { self.eth_protocol_version(peer_id) } } fn chain_overlay(&self) -> &RwLock<HashMap<BlockNumber, Bytes>> { diff --git a/util/network/src/host.rs b/util/network/src/host.rs index f5c14b0f9..a6d61d26f 100644 --- a/util/network/src/host.rs +++ b/util/network/src/host.rs @@ -155,6 +155,8 @@ pub enum NetworkIoMessage { protocol: ProtocolId, /// Supported protocol versions. versions: Vec<u8>, + /// Number of packet IDs reserved by the protocol. + packet_count: u8, }, /// Register a new protocol timer AddTimer { @@ -251,9 +253,8 @@ impl<'s> NetworkContext<'s> { self.io.channel() } - /// Disable current protocol capability for given peer. If no capabilities left peer gets disconnected. + /// Disconnect a peer and prevent it from connecting again. pub fn disable_peer(&self, peer: PeerId) { - //TODO: remove capability, disconnect if no capabilities left self.io.message(NetworkIoMessage::DisablePeer(peer)) .unwrap_or_else(|e| warn!("Error sending network IO message: {:?}", e)); } @@ -290,7 +291,7 @@ impl<'s> NetworkContext<'s> { } /// Returns max version for a given protocol. - pub fn protocol_version(&self, peer: PeerId, protocol: ProtocolId) -> Option<u8> { + pub fn protocol_version(&self, protocol: ProtocolId, peer: PeerId) -> Option<u8> { let session = self.resolve_session(peer); session.and_then(|s| s.lock().capability_version(protocol)) } @@ -1018,7 +1019,8 @@ impl IoHandler<NetworkIoMessage> for Host { NetworkIoMessage::AddHandler { ref handler, ref protocol, - ref versions + ref versions, + ref packet_count, } => { let h = handler.clone(); let reserved = self.reserved_nodes.read(); @@ -1026,7 +1028,7 @@ impl IoHandler<NetworkIoMessage> for Host { self.handlers.write().insert(*protocol, h); let mut info = self.info.write(); for v in versions { - info.capabilities.push(CapabilityInfo { protocol: *protocol, version: *v, packet_count:0 }); + info.capabilities.push(CapabilityInfo { protocol: *protocol, version: *v, packet_count: *packet_count }); } }, NetworkIoMessage::AddTimer { diff --git a/util/network/src/lib.rs b/util/network/src/lib.rs index 50396b6fb..627458c1c 100644 --- a/util/network/src/lib.rs +++ b/util/network/src/lib.rs @@ -45,7 +45,7 @@ //! //! fn main () { //! let mut service = NetworkService::new(NetworkConfiguration::new_local()).expect("Error creating network service"); -//! service.register_protocol(Arc::new(MyHandler), *b"myp", &[1u8]); +//! service.register_protocol(Arc::new(MyHandler), *b"myp", 1, &[1u8]); //! service.start().expect("Error starting service"); //! //! // Wait for quit condition @@ -91,13 +91,9 @@ mod ip_utils; #[cfg(test)] mod tests; -pub use host::PeerId; -pub use host::PacketId; -pub use host::NetworkContext; +pub use host::{PeerId, PacketId, ProtocolId, NetworkContext, NetworkIoMessage, NetworkConfiguration}; pub use service::NetworkService; -pub use host::NetworkIoMessage; pub use error::NetworkError; -pub use host::NetworkConfiguration; pub use stats::NetworkStats; pub use session::SessionInfo; diff --git a/util/network/src/service.rs b/util/network/src/service.rs index 0b59f8bc7..3fe6ae04a 100644 --- a/util/network/src/service.rs +++ b/util/network/src/service.rs @@ -73,11 +73,12 @@ impl NetworkService { } /// Regiter a new protocol handler with the event loop. - pub fn register_protocol(&self, handler: Arc<NetworkProtocolHandler + Send + Sync>, protocol: ProtocolId, versions: &[u8]) -> Result<(), NetworkError> { + pub fn register_protocol(&self, handler: Arc<NetworkProtocolHandler + Send + Sync>, protocol: ProtocolId, packet_count: u8, versions: &[u8]) -> Result<(), NetworkError> { try!(self.io_service.send_message(NetworkIoMessage::AddHandler { handler: handler, protocol: protocol, versions: versions.to_vec(), + packet_count: packet_count, })); Ok(()) } diff --git a/util/network/src/session.rs b/util/network/src/session.rs index c7f196680..1791a441d 100644 --- a/util/network/src/session.rs +++ b/util/network/src/session.rs @@ -16,6 +16,7 @@ use std::{str, io}; use std::net::SocketAddr; +use std::cmp::Ordering; use std::sync::*; use mio::*; use mio::tcp::*; @@ -122,7 +123,7 @@ impl ToString for PeerCapabilityInfo { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SessionCapabilityInfo { pub protocol: [u8; 3], pub version: u8, @@ -130,6 +131,23 @@ pub struct SessionCapabilityInfo { pub id_offset: u8, } +impl PartialOrd for SessionCapabilityInfo { + fn partial_cmp(&self, other: &SessionCapabilityInfo) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for SessionCapabilityInfo { + fn cmp(&self, b: &SessionCapabilityInfo) -> Ordering { + // By protocol id first + if self.protocol != b.protocol { + return self.protocol.cmp(&b.protocol); + } + // By version + self.version.cmp(&b.version) + } +} + const PACKET_HELLO: u8 = 0x80; const PACKET_DISCONNECT: u8 = 0x01; const PACKET_PING: u8 = 0x02; @@ -441,6 +459,9 @@ impl Session { } } + // Sort capabilities alphabeticaly. + caps.sort(); + i = 0; let mut offset: u8 = PACKET_USER; while i < caps.len() { diff --git a/util/network/src/tests.rs b/util/network/src/tests.rs index 1b0c6eb6c..467dbcbd8 100644 --- a/util/network/src/tests.rs +++ b/util/network/src/tests.rs @@ -41,7 +41,7 @@ impl TestProtocol { /// Creates and register protocol with the network service pub fn register(service: &mut NetworkService, drop_session: bool) -> Arc<TestProtocol> { let handler = Arc::new(TestProtocol::new(drop_session)); - service.register_protocol(handler.clone(), *b"tst", &[42u8, 43u8]).expect("Error registering test protocol handler"); + service.register_protocol(handler.clone(), *b"tst", 1, &[42u8, 43u8]).expect("Error registering test protocol handler"); handler } @@ -93,7 +93,7 @@ impl NetworkProtocolHandler for TestProtocol { fn net_service() { let service = NetworkService::new(NetworkConfiguration::new_local()).expect("Error creating network service"); service.start().unwrap(); - service.register_protocol(Arc::new(TestProtocol::new(false)), *b"myp", &[1u8]).unwrap(); + service.register_protocol(Arc::new(TestProtocol::new(false)), *b"myp", 1, &[1u8]).unwrap(); } #[test] From f543108cf52bc2abec50a09c730f380c148cb38d Mon Sep 17 00:00:00 2001 From: arkpar <arkady.paronyan@gmail.com> Date: Mon, 24 Oct 2016 16:31:37 +0200 Subject: [PATCH 24/77] Fixed tests --- ethcore/src/verification/queue/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ethcore/src/verification/queue/mod.rs b/ethcore/src/verification/queue/mod.rs index c4ee53c23..f801bbe2e 100644 --- a/ethcore/src/verification/queue/mod.rs +++ b/ethcore/src/verification/queue/mod.rs @@ -529,7 +529,7 @@ mod tests { fn get_test_queue() -> BlockQueue { let spec = get_test_spec(); let engine = spec.engine; - BlockQueue::new(Config::default(), engine, IoChannel::disconnected()) + BlockQueue::new(Config::default(), engine, IoChannel::disconnected(), true) } #[test] @@ -537,7 +537,7 @@ mod tests { // TODO better test let spec = Spec::new_test(); let engine = spec.engine; - let _ = BlockQueue::new(Config::default(), engine, IoChannel::disconnected()); + let _ = BlockQueue::new(Config::default(), engine, IoChannel::disconnected(), true); } #[test] @@ -601,7 +601,7 @@ mod tests { let engine = spec.engine; let mut config = Config::default(); config.max_mem_use = super::MIN_MEM_LIMIT; // empty queue uses about 15000 - let queue = BlockQueue::new(config, engine, IoChannel::disconnected()); + let queue = BlockQueue::new(config, engine, IoChannel::disconnected(), true); assert!(!queue.queue_info().is_full()); let mut blocks = get_good_dummy_block_seq(50); for b in blocks.drain(..) { From 487da9c9c636d75a7b1d29bab6cad13b55f925d4 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac <ngotchac@gmail.com> Date: Mon, 24 Oct 2016 16:35:43 +0200 Subject: [PATCH 25/77] Trigger accounts/contracts search on search input change (#2838) * Styling Chips in search bar (#2766) * Styling search chips // Add chip on space/comma/... (#2766) * Update search on input (#2766) * Fixing search triggers bugs (#2766) * removed console logs * Use props instead of weird CSS selectors for Search Bar * Add tags on space and commas in EditMeta modal (#2766) * Fixed empty input in EditMeta modal ; tokens input --- js/src/modals/EditMeta/editMeta.js | 43 ++++++++++-- js/src/ui/Actionbar/Search/search.css | 8 +++ js/src/ui/Actionbar/Search/search.js | 99 +++++++++++++++++++++------ js/src/views/Accounts/accounts.js | 15 ++-- js/src/views/Addresses/addresses.js | 15 ++-- js/src/views/Contracts/contracts.js | 15 ++-- 6 files changed, 149 insertions(+), 46 deletions(-) diff --git a/js/src/modals/EditMeta/editMeta.js b/js/src/modals/EditMeta/editMeta.js index 103208945..aef0232a6 100644 --- a/js/src/modals/EditMeta/editMeta.js +++ b/js/src/modals/EditMeta/editMeta.js @@ -36,7 +36,7 @@ export default class EditMeta extends Component { } state = { - meta: this.props.account.meta, + meta: Object.assign({}, this.props.account.meta), metaErrors: {}, name: this.props.account.name, nameError: null @@ -102,20 +102,55 @@ export default class EditMeta extends Component { renderTags () { const { meta } = this.state; const { tags } = meta || []; - const onChange = (chips) => this.onMetaChange('tags', chips); return ( <ChipInput - defaultValue={ tags } - onChange={ onChange } + ref='tagsInput' + value={ tags } + onRequestAdd={ this.onAddTag } + onRequestDelete={ this.onDeleteTag } floatingLabelText='(optional) tags' hintText='press <Enter> to add a tag' + onUpdateInput={ this.onTagsInputChange } floatingLabelFixed fullWidth /> ); } + onAddTag = (tag) => { + const { meta } = this.state; + const { tags } = meta || []; + + this.onMetaChange('tags', [].concat(tags, tag)); + } + + onDeleteTag = (tag) => { + const { meta } = this.state; + const { tags } = meta || []; + + const newTags = tags + .filter(t => t !== tag); + + this.onMetaChange('tags', newTags); + } + + onTagsInputChange = (value) => { + const { meta } = this.state; + const { tags } = meta || []; + + const tokens = value.split(/[\s,;]+/); + + const newTokens = tokens + .slice(0, -1) + .filter(t => t.length > 0); + + const inputValue = tokens.slice(-1)[0].trim(); + + this.onMetaChange('tags', [].concat(tags, newTokens)); + this.refs.tagsInput.setState({ inputValue }); + } + onNameChange = (name) => { this.setState(validateName(name)); } diff --git a/js/src/ui/Actionbar/Search/search.css b/js/src/ui/Actionbar/Search/search.css index 956a9e2e3..58365d03b 100644 --- a/js/src/ui/Actionbar/Search/search.css +++ b/js/src/ui/Actionbar/Search/search.css @@ -41,3 +41,11 @@ width: 0; height: 0; } + +.chip > svg { + width: 1.2rem !important; + height: 1.2rem !important; + margin: initial !important; + margin-right: 4px !important; + padding: 4px 0 !important; +} diff --git a/js/src/ui/Actionbar/Search/search.js b/js/src/ui/Actionbar/Search/search.js index 79fabebc8..e00fd433f 100644 --- a/js/src/ui/Actionbar/Search/search.js +++ b/js/src/ui/Actionbar/Search/search.js @@ -15,6 +15,8 @@ // along with Parity. If not, see <http://www.gnu.org/licenses/>. import React, { Component, PropTypes } from 'react'; +import { Chip } from 'material-ui'; +import { blue300 } from 'material-ui/styles/colors'; // import ChipInput from 'material-ui-chip-input'; import ChipInput from 'material-ui-chip-input/src/ChipInput'; import ActionSearch from 'material-ui/svg-icons/action/search'; @@ -43,6 +45,10 @@ export default class ActionbarSearch extends Component { if (tokens.length > 0 && this.props.tokens.length === 0) { this.handleOpenSearch(true, true); } + + if (tokens.length !== this.props.tokens.length) { + this.handleSearchChange(tokens); + } } componentWillUnmount () { @@ -71,16 +77,26 @@ export default class ActionbarSearch extends Component { <ChipInput clearOnBlur={ false } className={ styles.input } + chipRenderer={ this.chipRenderer } hintText='Enter search input...' - hintStyle={ { - transition: 'none' - } } ref='searchInput' value={ tokens } onBlur={ this.handleSearchBlur } onRequestAdd={ this.handleTokenAdd } onRequestDelete={ this.handleTokenDelete } - onUpdateInput={ this.handleInputChange } /> + onUpdateInput={ this.handleInputChange } + hintStyle={ { + bottom: 16, + left: 2, + transition: 'none' + } } + inputStyle={ { + marginBottom: 18 + } } + textFieldStyle={ { + height: 42 + } } + /> </div> <Button @@ -92,42 +108,83 @@ export default class ActionbarSearch extends Component { ); } + chipRenderer = (state, key) => { + const { value, isFocused, isDisabled, handleClick, handleRequestDelete } = state; + + return ( + <Chip + key={ key } + className={ styles.chip } + style={ { + margin: '8px 8px 0 0', + float: 'left', + pointerEvents: isDisabled ? 'none' : undefined, + alignItems: 'center' + } } + labelStyle={ { + paddingRight: 6, + fontSize: '0.9rem', + lineHeight: 'initial' + } } + backgroundColor={ isFocused ? blue300 : 'rgba(0, 0, 0, 0.73)' } + onTouchTap={ handleClick } + onRequestDelete={ handleRequestDelete } + > + { value } + </Chip> + ); + } + handleTokenAdd = (value) => { const { tokens } = this.props; - const newSearchValues = uniq([].concat(tokens, value)); + const newSearchTokens = uniq([].concat(tokens, value)); - this.setState({ - inputValue: '' - }); - - this.handleSearchChange(newSearchValues); + this.handleSearchChange(newSearchTokens); } handleTokenDelete = (value) => { const { tokens } = this.props; - const newSearchValues = [] + const newSearchTokens = [] .concat(tokens) .filter(v => v !== value); - this.setState({ - inputValue: '' - }); - - this.handleSearchChange(newSearchValues); + this.handleSearchChange(newSearchTokens); this.refs.searchInput.focus(); } handleInputChange = (value) => { - this.setState({ inputValue: value }); + const splitTokens = value.split(/[\s,;]/); + + const inputValue = (splitTokens.length <= 1) + ? value + : splitTokens.slice(-1)[0].trim(); + + this.refs.searchInput.setState({ inputValue }); + this.setState({ inputValue }, () => { + if (splitTokens.length > 1) { + const tokensToAdd = splitTokens.slice(0, -1); + tokensToAdd.forEach(token => this.handleTokenAdd(token)); + } else { + this.handleSearchChange(); + } + }); } - handleSearchChange = (searchValues) => { - const { onChange } = this.props; - const newSearchValues = searchValues.filter(v => v.length > 0); + handleSearchChange = (searchTokens) => { + const { onChange, tokens } = this.props; + const { inputValue } = this.state; - onChange(newSearchValues); + const newSearchTokens = [] + .concat(searchTokens || tokens) + .filter(v => v.length > 0); + + const newSearchValues = [] + .concat(searchTokens || tokens, inputValue) + .filter(v => v.length > 0); + + onChange(newSearchTokens, newSearchValues); } handleSearchClick = () => { diff --git a/js/src/views/Accounts/accounts.js b/js/src/views/Accounts/accounts.js index cc398c313..2cf2c3adf 100644 --- a/js/src/views/Accounts/accounts.js +++ b/js/src/views/Accounts/accounts.js @@ -41,7 +41,8 @@ class Accounts extends Component { addressBook: false, newDialog: false, sortOrder: '', - searchValues: [] + searchValues: [], + searchTokens: [] } render () { @@ -69,14 +70,14 @@ class Accounts extends Component { } renderSearchButton () { - const onChange = (searchValues) => { - this.setState({ searchValues }); + const onChange = (searchTokens, searchValues) => { + this.setState({ searchTokens, searchValues }); }; return ( <ActionbarSearch key='searchAccount' - tokens={ this.state.searchValues } + tokens={ this.state.searchTokens } onChange={ onChange } /> ); } @@ -136,9 +137,9 @@ class Accounts extends Component { } onAddSearchToken = (token) => { - const { searchValues } = this.state; - const newSearchValues = uniq([].concat(searchValues, token)); - this.setState({ searchValues: newSearchValues }); + const { searchTokens } = this.state; + const newSearchTokens = uniq([].concat(searchTokens, token)); + this.setState({ searchTokens: newSearchTokens }); } onNewAccountClick = () => { diff --git a/js/src/views/Addresses/addresses.js b/js/src/views/Addresses/addresses.js index 9907c452d..2ddb53881 100644 --- a/js/src/views/Addresses/addresses.js +++ b/js/src/views/Addresses/addresses.js @@ -40,7 +40,8 @@ class Addresses extends Component { state = { showAdd: false, sortOrder: '', - searchValues: [] + searchValues: [], + searchTokens: [] } render () { @@ -79,14 +80,14 @@ class Addresses extends Component { } renderSearchButton () { - const onChange = (searchValues) => { - this.setState({ searchValues }); + const onChange = (searchTokens, searchValues) => { + this.setState({ searchTokens, searchValues }); }; return ( <ActionbarSearch key='searchAddress' - tokens={ this.state.searchValues } + tokens={ this.state.searchTokens } onChange={ onChange } /> ); } @@ -127,9 +128,9 @@ class Addresses extends Component { } onAddSearchToken = (token) => { - const { searchValues } = this.state; - const newSearchValues = uniq([].concat(searchValues, token)); - this.setState({ searchValues: newSearchValues }); + const { searchTokens } = this.state; + const newSearchTokens = uniq([].concat(searchTokens, token)); + this.setState({ searchTokens: newSearchTokens }); } onOpenAdd = () => { diff --git a/js/src/views/Contracts/contracts.js b/js/src/views/Contracts/contracts.js index 876aaadb2..f4cbc6dcc 100644 --- a/js/src/views/Contracts/contracts.js +++ b/js/src/views/Contracts/contracts.js @@ -43,7 +43,8 @@ class Contracts extends Component { addContract: false, deployContract: false, sortOrder: '', - searchValues: [] + searchValues: [], + searchTokens: [] } render () { @@ -84,14 +85,14 @@ class Contracts extends Component { } renderSearchButton () { - const onChange = (searchValues) => { - this.setState({ searchValues }); + const onChange = (searchTokens, searchValues) => { + this.setState({ searchTokens, searchValues }); }; return ( <ActionbarSearch key='searchContract' - tokens={ this.state.searchValues } + tokens={ this.state.searchTokens } onChange={ onChange } /> ); } @@ -152,9 +153,9 @@ class Contracts extends Component { } onAddSearchToken = (token) => { - const { searchValues } = this.state; - const newSearchValues = uniq([].concat(searchValues, token)); - this.setState({ searchValues: newSearchValues }); + const { searchTokens } = this.state; + const newSearchTokens = uniq([].concat(searchTokens, token)); + this.setState({ searchTokens: newSearchTokens }); } onDeployContractClose = () => { From 16ec413508dc4e9c0e5700170d50fbf7d85e505f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= <tomusdrw@users.noreply.github.com> Date: Mon, 24 Oct 2016 16:36:15 +0200 Subject: [PATCH 26/77] Local dapps embeddable on signer port (#2815) --- dapps/src/apps/fetcher.rs | 12 ++++++++---- dapps/src/apps/fs.rs | 4 ++-- dapps/src/apps/mod.rs | 2 +- dapps/src/lib.rs | 2 +- dapps/src/page/local.rs | 9 ++++++--- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/dapps/src/apps/fetcher.rs b/dapps/src/apps/fetcher.rs index 210c6b180..e4ee63fea 100644 --- a/dapps/src/apps/fetcher.rs +++ b/dapps/src/apps/fetcher.rs @@ -46,6 +46,7 @@ pub struct ContentFetcher<R: URLHint = URLHintContract> { resolver: R, cache: Arc<Mutex<ContentCache>>, sync: Arc<SyncStatus>, + embeddable_at: Option<u16>, } impl<R: URLHint> Drop for ContentFetcher<R> { @@ -57,7 +58,7 @@ impl<R: URLHint> Drop for ContentFetcher<R> { impl<R: URLHint> ContentFetcher<R> { - pub fn new(resolver: R, sync_status: Arc<SyncStatus>) -> Self { + pub fn new(resolver: R, sync_status: Arc<SyncStatus>, embeddable_at: Option<u16>) -> Self { let mut dapps_path = env::temp_dir(); dapps_path.push(random_filename()); @@ -66,6 +67,7 @@ impl<R: URLHint> ContentFetcher<R> { resolver: resolver, sync: sync_status, cache: Arc::new(Mutex::new(ContentCache::default())), + embeddable_at: embeddable_at, } } @@ -152,6 +154,7 @@ impl<R: URLHint> ContentFetcher<R> { id: content_id.clone(), dapps_path: self.dapps_path.clone(), on_done: Box::new(on_done), + embeddable_at: self.embeddable_at, } ); @@ -276,6 +279,7 @@ struct DappInstaller { id: String, dapps_path: PathBuf, on_done: Box<Fn(String, Option<LocalPageEndpoint>) + Send>, + embeddable_at: Option<u16>, } impl DappInstaller { @@ -368,7 +372,7 @@ impl ContentValidator for DappInstaller { try!(manifest_file.write_all(manifest_str.as_bytes())); // Create endpoint - let app = LocalPageEndpoint::new(target, manifest.clone().into()); + let app = LocalPageEndpoint::new(target, manifest.clone().into(), self.embeddable_at); // Return modified app manifest Ok((manifest.id.clone(), app)) @@ -401,14 +405,14 @@ mod tests { fn should_true_if_contains_the_app() { // given let path = env::temp_dir(); - let fetcher = ContentFetcher::new(FakeResolver, Arc::new(|| false)); + let fetcher = ContentFetcher::new(FakeResolver, Arc::new(|| false), None); let handler = LocalPageEndpoint::new(path, EndpointInfo { name: "fake".into(), description: "".into(), version: "".into(), author: "".into(), icon_url: "".into(), - }); + }, None); // when fetcher.set_status("test", ContentStatus::Ready(handler)); diff --git a/dapps/src/apps/fs.rs b/dapps/src/apps/fs.rs index 4728757de..e7a11fc8e 100644 --- a/dapps/src/apps/fs.rs +++ b/dapps/src/apps/fs.rs @@ -97,12 +97,12 @@ fn read_manifest(name: &str, mut path: PathBuf) -> EndpointInfo { }) } -pub fn local_endpoints(dapps_path: String) -> Endpoints { +pub fn local_endpoints(dapps_path: String, signer_port: Option<u16>) -> Endpoints { let mut pages = Endpoints::new(); for dapp in local_dapps(dapps_path) { pages.insert( dapp.id, - Box::new(LocalPageEndpoint::new(dapp.path, dapp.info)) + Box::new(LocalPageEndpoint::new(dapp.path, dapp.info, signer_port)) ); } pages diff --git a/dapps/src/apps/mod.rs b/dapps/src/apps/mod.rs index 40ddb7064..a7b97d37c 100644 --- a/dapps/src/apps/mod.rs +++ b/dapps/src/apps/mod.rs @@ -47,7 +47,7 @@ pub fn utils() -> Box<Endpoint> { pub fn all_endpoints(dapps_path: String, signer_port: Option<u16>) -> Endpoints { // fetch fs dapps at first to avoid overwriting builtins - let mut pages = fs::local_endpoints(dapps_path); + let mut pages = fs::local_endpoints(dapps_path, signer_port); // NOTE [ToDr] Dapps will be currently embeded on 8180 insert::<parity_ui::App>(&mut pages, "ui", Embeddable::Yes(signer_port)); diff --git a/dapps/src/lib.rs b/dapps/src/lib.rs index 00c8b275e..0041dbedf 100644 --- a/dapps/src/lib.rs +++ b/dapps/src/lib.rs @@ -218,7 +218,7 @@ impl Server { ) -> Result<Server, ServerError> { let panic_handler = Arc::new(Mutex::new(None)); let authorization = Arc::new(authorization); - let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(apps::urlhint::URLHintContract::new(registrar), sync_status)); + let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(apps::urlhint::URLHintContract::new(registrar), sync_status, signer_port)); let endpoints = Arc::new(apps::all_endpoints(dapps_path, signer_port.clone())); let cors_domains = Self::cors_domains(signer_port); diff --git a/dapps/src/page/local.rs b/dapps/src/page/local.rs index e702fe4d5..5390f5aac 100644 --- a/dapps/src/page/local.rs +++ b/dapps/src/page/local.rs @@ -26,14 +26,16 @@ pub struct LocalPageEndpoint { path: PathBuf, mime: Option<String>, info: Option<EndpointInfo>, + embeddable_at: Option<u16>, } impl LocalPageEndpoint { - pub fn new(path: PathBuf, info: EndpointInfo) -> Self { + pub fn new(path: PathBuf, info: EndpointInfo, embeddable_at: Option<u16>) -> Self { LocalPageEndpoint { path: path, mime: None, info: Some(info), + embeddable_at: embeddable_at, } } @@ -42,6 +44,7 @@ impl LocalPageEndpoint { path: path, mime: Some(mime), info: None, + embeddable_at: None, } } @@ -62,7 +65,7 @@ impl Endpoint for LocalPageEndpoint { prefix: None, path: path, file: handler::ServedFile::new(None), - safe_to_embed_at_port: None, + safe_to_embed_at_port: self.embeddable_at, }) } else { Box::new(handler::PageHandler { @@ -70,7 +73,7 @@ impl Endpoint for LocalPageEndpoint { prefix: None, path: path, file: handler::ServedFile::new(None), - safe_to_embed_at_port: None, + safe_to_embed_at_port: self.embeddable_at, }) } } From 7f210b05bb929c3f9b60133e5dea441522791851 Mon Sep 17 00:00:00 2001 From: Robert Habermeier <rphmeier@gmail.com> Date: Mon, 24 Oct 2016 16:38:32 +0200 Subject: [PATCH 27/77] fix failing master test build (#2846) From 1a5bae8ef10deee053fd5681147c2d45d37cc1bb Mon Sep 17 00:00:00 2001 From: Arkadiy Paronyan <arkady.paronyan@gmail.com> Date: Mon, 24 Oct 2016 18:25:27 +0200 Subject: [PATCH 28/77] Extended network options (#2845) * More network configuration options * Filter UDP requests * Fixed tests * Fixed test warning --- parity/cli/config.full.toml | 3 ++ parity/cli/config.toml | 3 ++ parity/cli/mod.rs | 15 ++++++++++ parity/cli/usage.txt | 10 ++++++- parity/configuration.rs | 22 ++++++++++++++- parity/helpers.rs | 5 +++- sync/src/api.rs | 51 ++++++++++++++++++++++++++++++++-- sync/src/lib.rs | 2 +- util/network/src/discovery.rs | 37 ++++++++++++++++-------- util/network/src/host.rs | 43 +++++++++++++++++++--------- util/network/src/ip_utils.rs | 16 +++++++++++ util/network/src/lib.rs | 12 ++++++++ util/network/src/node_table.rs | 29 +++++++++---------- util/src/kvdb.rs | 2 +- 14 files changed, 204 insertions(+), 46 deletions(-) diff --git a/parity/cli/config.full.toml b/parity/cli/config.full.toml index b82762684..fd2e11f98 100644 --- a/parity/cli/config.full.toml +++ b/parity/cli/config.full.toml @@ -29,6 +29,9 @@ id = "0x1" bootnodes = [] discovery = true warp = true +allow_ips = "all" +snapshot_peers = 0 +max_pending_peers = 64 reserved_only = false reserved_peers = "./path_to_file" diff --git a/parity/cli/config.toml b/parity/cli/config.toml index 5fcd4ce73..e6f01e1ae 100644 --- a/parity/cli/config.toml +++ b/parity/cli/config.toml @@ -18,6 +18,9 @@ discovery = true nat = "any" min_peers = 10 max_peers = 20 +max_pending_peers = 30 +snapshot_peers = 40 +allow_ips = "public" reserved_only = true reserved_peers = "./path/to/reserved_peers" diff --git a/parity/cli/mod.rs b/parity/cli/mod.rs index d4558a50d..27e0cb4dc 100644 --- a/parity/cli/mod.rs +++ b/parity/cli/mod.rs @@ -114,8 +114,14 @@ usage! { or |c: &Config| otry!(c.network).min_peers.clone(), flag_max_peers: u16 = 50u16, or |c: &Config| otry!(c.network).max_peers.clone(), + flag_max_pending_peers: u16 = 64u16, + or |c: &Config| otry!(c.network).max_pending_peers.clone(), + flag_snapshot_peers: u16 = 0u16, + or |c: &Config| otry!(c.network).snapshot_peers.clone(), flag_nat: String = "any", or |c: &Config| otry!(c.network).nat.clone(), + flag_allow_ips: String = "all", + or |c: &Config| otry!(c.network).allow_ips.clone(), flag_network_id: Option<String> = None, or |c: &Config| otry!(c.network).id.clone().map(Some), flag_bootnodes: Option<String> = None, @@ -307,7 +313,10 @@ struct Network { port: Option<u16>, min_peers: Option<u16>, max_peers: Option<u16>, + snapshot_peers: Option<u16>, + max_pending_peers: Option<u16>, nat: Option<String>, + allow_ips: Option<String>, id: Option<String>, bootnodes: Option<Vec<String>>, discovery: Option<bool>, @@ -494,6 +503,9 @@ mod tests { flag_port: 30303u16, flag_min_peers: 25u16, flag_max_peers: 50u16, + flag_max_pending_peers: 64u16, + flag_snapshot_peers: 0u16, + flag_allow_ips: "all".into(), flag_nat: "any".into(), flag_network_id: Some("0x1".into()), flag_bootnodes: Some("".into()), @@ -653,6 +665,9 @@ mod tests { port: None, min_peers: Some(10), max_peers: Some(20), + max_pending_peers: Some(30), + snapshot_peers: Some(40), + allow_ips: Some("public".into()), nat: Some("any".into()), id: None, bootnodes: None, diff --git a/parity/cli/usage.txt b/parity/cli/usage.txt index fe6203842..af8e83f0e 100644 --- a/parity/cli/usage.txt +++ b/parity/cli/usage.txt @@ -71,7 +71,9 @@ Networking Options: --port PORT Override the port on which the node should listen (default: {flag_port}). --min-peers NUM Try to maintain at least NUM peers (default: {flag_min_peers}). - --max-peers NUM Allow up to that many peers (default: {flag_max_peers}). + --max-peers NUM Allow up to NUM peers (default: {flag_max_peers}). + --snapshot-peers NUM Allow additional NUM peers for a snapshot sync + (default: {flag_snapshot_peers}). --nat METHOD Specify method to use for determining public address. Must be one of: any, none, upnp, extip:<IP> (default: {flag_nat}). @@ -86,6 +88,12 @@ Networking Options: These nodes will always have a reserved slot on top of the normal maximum peers. (default: {flag_reserved_peers:?}) --reserved-only Connect only to reserved nodes. (default: {flag_reserved_only}) + --allow-ips FILTER Filter outbound connections. Must be one of: + private - connect to private network IP addresses only; + public - connect to public network IP addresses only; + all - connect to any IP address. + (default: {flag_allow_ips}) + --max-pending-peers NUM Allow up to NUM pending connections. (default: {flag_max_pending_peers}) API and Console Options: --no-jsonrpc Disable the JSON-RPC API server. (default: {flag_no_jsonrpc}) diff --git a/parity/configuration.rs b/parity/configuration.rs index 060679b0e..5680e6110 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -22,7 +22,7 @@ use std::cmp::max; use cli::{Args, ArgsError}; use util::{Hashable, U256, Uint, Bytes, version_data, Secret, Address}; use util::log::Colour; -use ethsync::{NetworkConfiguration, is_valid_node_url}; +use ethsync::{NetworkConfiguration, is_valid_node_url, AllowIP}; use ethcore::client::{VMType, Mode}; use ethcore::miner::MinerOptions; @@ -332,10 +332,27 @@ impl Configuration { max(self.min_peers(), peers) } + fn allow_ips(&self) -> Result<AllowIP, String> { + match self.args.flag_allow_ips.as_str() { + "all" => Ok(AllowIP::All), + "public" => Ok(AllowIP::Public), + "private" => Ok(AllowIP::Private), + _ => Err("Invalid IP filter value".to_owned()), + } + } + fn min_peers(&self) -> u32 { self.args.flag_peers.unwrap_or(self.args.flag_min_peers) as u32 } + fn max_pending_peers(&self) -> u32 { + self.args.flag_max_pending_peers as u32 + } + + fn snapshot_peers(&self) -> u32 { + self.args.flag_snapshot_peers as u32 + } + fn work_notify(&self) -> Vec<String> { self.args.flag_notify_work.as_ref().map_or_else(Vec::new, |s| s.split(',').map(|s| s.to_owned()).collect()) } @@ -474,6 +491,9 @@ impl Configuration { ret.discovery_enabled = !self.args.flag_no_discovery && !self.args.flag_nodiscover; ret.max_peers = self.max_peers(); ret.min_peers = self.min_peers(); + ret.snapshot_peers = self.snapshot_peers(); + ret.allow_ips = try!(self.allow_ips()); + ret.max_pending_peers = self.max_pending_peers(); let mut net_path = PathBuf::from(self.directories().db); net_path.push("network"); ret.config_path = Some(net_path.to_str().unwrap().to_owned()); diff --git a/parity/helpers.rs b/parity/helpers.rs index a965314f5..5d6859f5b 100644 --- a/parity/helpers.rs +++ b/parity/helpers.rs @@ -185,7 +185,7 @@ pub fn to_bootnodes(bootnodes: &Option<String>) -> Result<Vec<String>, String> { #[cfg(test)] pub fn default_network_config() -> ::ethsync::NetworkConfiguration { - use ethsync::NetworkConfiguration; + use ethsync::{NetworkConfiguration, AllowIP}; NetworkConfiguration { config_path: Some(replace_home("$HOME/.parity/network")), net_config_path: None, @@ -198,6 +198,9 @@ pub fn default_network_config() -> ::ethsync::NetworkConfiguration { use_secret: None, max_peers: 50, min_peers: 25, + snapshot_peers: 0, + max_pending_peers: 64, + allow_ips: AllowIP::All, reserved_nodes: Vec::new(), allow_non_reserved: true, } diff --git a/sync/src/api.rs b/sync/src/api.rs index d0d734024..fb8fdc691 100644 --- a/sync/src/api.rs +++ b/sync/src/api.rs @@ -18,7 +18,8 @@ use std::sync::Arc; use std::collections::HashMap; use util::Bytes; use network::{NetworkProtocolHandler, NetworkService, NetworkContext, PeerId, ProtocolId, - NetworkConfiguration as BasicNetworkConfiguration, NonReservedPeerMode, NetworkError}; + NetworkConfiguration as BasicNetworkConfiguration, NonReservedPeerMode, NetworkError, + AllowIP as NetworkAllowIP}; use util::{U256, H256}; use io::{TimerToken}; use ethcore::client::{BlockChainClient, ChainNotify}; @@ -102,13 +103,15 @@ pub struct EthSync { handler: Arc<SyncProtocolHandler>, /// The main subprotocol name subprotocol_name: [u8; 3], + /// Configuration + config: NetworkConfiguration, } impl EthSync { /// Creates and register protocol with the network service pub fn new(config: SyncConfig, chain: Arc<BlockChainClient>, snapshot_service: Arc<SnapshotService>, network_config: NetworkConfiguration) -> Result<Arc<EthSync>, NetworkError> { let chain_sync = ChainSync::new(config, &*chain); - let service = try!(NetworkService::new(try!(network_config.into_basic()))); + let service = try!(NetworkService::new(try!(network_config.clone().into_basic()))); let sync = Arc::new(EthSync{ network: service, handler: Arc::new(SyncProtocolHandler { @@ -118,6 +121,7 @@ impl EthSync { overlay: RwLock::new(HashMap::new()), }), subprotocol_name: config.subprotocol_name, + config: network_config, }); Ok(sync) @@ -276,6 +280,29 @@ impl ManageNetwork for EthSync { } } +/// IP fiter +#[derive(Binary, Clone, Debug, PartialEq, Eq)] +pub enum AllowIP { + /// Connect to any address + All, + /// Connect to private network only + Private, + /// Connect to public network only + Public, +} + +impl AllowIP { + /// Attempt to parse the peer mode from a string. + pub fn parse(s: &str) -> Option<Self> { + match s { + "all" => Some(AllowIP::All), + "private" => Some(AllowIP::Private), + "public" => Some(AllowIP::Public), + _ => None, + } + } +} + #[derive(Binary, Debug, Clone, PartialEq, Eq)] /// Network service configuration pub struct NetworkConfiguration { @@ -301,10 +328,16 @@ pub struct NetworkConfiguration { pub max_peers: u32, /// Min number of connected peers to maintain pub min_peers: u32, + /// Max pending peers. + pub max_pending_peers: u32, + /// Reserved snapshot sync peers. + pub snapshot_peers: u32, /// List of reserved node addresses. pub reserved_nodes: Vec<String>, /// The non-reserved peer mode. pub allow_non_reserved: bool, + /// IP Filtering + pub allow_ips: AllowIP, } impl NetworkConfiguration { @@ -340,7 +373,14 @@ impl NetworkConfiguration { use_secret: self.use_secret, max_peers: self.max_peers, min_peers: self.min_peers, + max_handshakes: self.max_pending_peers, + reserved_protocols: hash_map![WARP_SYNC_PROTOCOL_ID => self.snapshot_peers], reserved_nodes: self.reserved_nodes, + allow_ips: match self.allow_ips { + AllowIP::All => NetworkAllowIP::All, + AllowIP::Private => NetworkAllowIP::Private, + AllowIP::Public => NetworkAllowIP::Public, + }, non_reserved_mode: if self.allow_non_reserved { NonReservedPeerMode::Accept } else { NonReservedPeerMode::Deny }, }) } @@ -360,7 +400,14 @@ impl From<BasicNetworkConfiguration> for NetworkConfiguration { use_secret: other.use_secret, max_peers: other.max_peers, min_peers: other.min_peers, + max_pending_peers: other.max_handshakes, + snapshot_peers: *other.reserved_protocols.get(&WARP_SYNC_PROTOCOL_ID).unwrap_or(&0), reserved_nodes: other.reserved_nodes, + allow_ips: match other.allow_ips { + NetworkAllowIP::All => AllowIP::All, + NetworkAllowIP::Private => AllowIP::Private, + NetworkAllowIP::Public => AllowIP::Public, + }, allow_non_reserved: match other.non_reserved_mode { NonReservedPeerMode::Accept => true, _ => false } , } } diff --git a/sync/src/lib.rs b/sync/src/lib.rs index 6cfe2a26c..532c05711 100644 --- a/sync/src/lib.rs +++ b/sync/src/lib.rs @@ -61,7 +61,7 @@ mod api { } pub use api::{EthSync, SyncProvider, SyncClient, NetworkManagerClient, ManageNetwork, SyncConfig, - ServiceConfiguration, NetworkConfiguration, PeerInfo}; + ServiceConfiguration, NetworkConfiguration, PeerInfo, AllowIP}; pub use chain::{SyncStatus, SyncState}; pub use network::{is_valid_node_url, NonReservedPeerMode, NetworkError}; diff --git a/util/network/src/discovery.rs b/util/network/src/discovery.rs index 61eaf4094..595ac7605 100644 --- a/util/network/src/discovery.rs +++ b/util/network/src/discovery.rs @@ -29,6 +29,7 @@ use node_table::*; use error::NetworkError; use io::{StreamToken, IoContext}; use ethkey::{Secret, KeyPair, sign, recover}; +use AllowIP; use PROTOCOL_VERSION; @@ -95,6 +96,7 @@ pub struct Discovery { send_queue: VecDeque<Datagramm>, check_timestamps: bool, adding_nodes: Vec<NodeEntry>, + allow_ips: AllowIP, } pub struct TableUpdates { @@ -103,7 +105,7 @@ pub struct TableUpdates { } impl Discovery { - pub fn new(key: &KeyPair, listen: SocketAddr, public: NodeEndpoint, token: StreamToken) -> Discovery { + pub fn new(key: &KeyPair, listen: SocketAddr, public: NodeEndpoint, token: StreamToken, allow_ips: AllowIP) -> Discovery { let socket = UdpSocket::bound(&listen).expect("Error binding UDP socket"); Discovery { id: key.public().clone(), @@ -118,14 +120,17 @@ impl Discovery { send_queue: VecDeque::new(), check_timestamps: true, adding_nodes: Vec::new(), + allow_ips: allow_ips, } } /// Add a new node to discovery table. Pings the node. pub fn add_node(&mut self, e: NodeEntry) { - let endpoint = e.endpoint.clone(); - self.update_node(e); - self.ping(&endpoint); + if e.endpoint.is_allowed(self.allow_ips) { + let endpoint = e.endpoint.clone(); + self.update_node(e); + self.ping(&endpoint); + } } /// Add a list of nodes. Pings a few nodes each round @@ -137,7 +142,9 @@ impl Discovery { /// Add a list of known nodes to the table. pub fn init_node_list(&mut self, mut nodes: Vec<NodeEntry>) { for n in nodes.drain(..) { - self.update_node(n); + if n.endpoint.is_allowed(self.allow_ips) { + self.update_node(n); + } } } @@ -394,10 +401,11 @@ impl Discovery { try!(self.check_timestamp(timestamp)); let mut added_map = HashMap::new(); let entry = NodeEntry { id: node.clone(), endpoint: source.clone() }; - if !entry.endpoint.is_valid() || !entry.endpoint.is_global() { + if !entry.endpoint.is_valid() { debug!(target: "discovery", "Got bad address: {:?}", entry); - } - else { + } else if !entry.endpoint.is_allowed(self.allow_ips) { + debug!(target: "discovery", "Address not allowed: {:?}", entry); + } else { self.update_node(entry.clone()); added_map.insert(node.clone(), entry); } @@ -470,6 +478,10 @@ impl Discovery { debug!(target: "discovery", "Bad address: {:?}", endpoint); continue; } + if !endpoint.is_allowed(self.allow_ips) { + debug!(target: "discovery", "Address not allowed: {:?}", endpoint); + continue; + } let node_id: NodeId = try!(r.val_at(3)); if node_id == self.id { continue; @@ -539,6 +551,7 @@ mod tests { use std::str::FromStr; use rustc_serialize::hex::FromHex; use ethkey::{Random, Generator}; + use AllowIP; #[test] fn find_node() { @@ -563,8 +576,8 @@ mod tests { let key2 = Random.generate().unwrap(); let ep1 = NodeEndpoint { address: SocketAddr::from_str("127.0.0.1:40444").unwrap(), udp_port: 40444 }; let ep2 = NodeEndpoint { address: SocketAddr::from_str("127.0.0.1:40445").unwrap(), udp_port: 40445 }; - let mut discovery1 = Discovery::new(&key1, ep1.address.clone(), ep1.clone(), 0); - let mut discovery2 = Discovery::new(&key2, ep2.address.clone(), ep2.clone(), 0); + let mut discovery1 = Discovery::new(&key1, ep1.address.clone(), ep1.clone(), 0, AllowIP::All); + let mut discovery2 = Discovery::new(&key2, ep2.address.clone(), ep2.clone(), 0, AllowIP::All); let node1 = Node::from_str("enode://a979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@127.0.0.1:7770").unwrap(); let node2 = Node::from_str("enode://b979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@127.0.0.1:7771").unwrap(); @@ -596,7 +609,7 @@ mod tests { fn removes_expired() { let key = Random.generate().unwrap(); let ep = NodeEndpoint { address: SocketAddr::from_str("127.0.0.1:40446").unwrap(), udp_port: 40447 }; - let mut discovery = Discovery::new(&key, ep.address.clone(), ep.clone(), 0); + let mut discovery = Discovery::new(&key, ep.address.clone(), ep.clone(), 0, AllowIP::All); for _ in 0..1200 { discovery.add_node(NodeEntry { id: NodeId::random(), endpoint: ep.clone() }); } @@ -624,7 +637,7 @@ mod tests { fn packets() { let key = Random.generate().unwrap(); let ep = NodeEndpoint { address: SocketAddr::from_str("127.0.0.1:40447").unwrap(), udp_port: 40447 }; - let mut discovery = Discovery::new(&key, ep.address.clone(), ep.clone(), 0); + let mut discovery = Discovery::new(&key, ep.address.clone(), ep.clone(), 0, AllowIP::All); discovery.check_timestamps = false; let from = SocketAddr::from_str("99.99.99.99:40445").unwrap(); diff --git a/util/network/src/host.rs b/util/network/src/host.rs index a6d61d26f..866534397 100644 --- a/util/network/src/host.rs +++ b/util/network/src/host.rs @@ -34,7 +34,7 @@ use rlp::*; use session::{Session, SessionInfo, SessionData}; use error::*; use io::*; -use {NetworkProtocolHandler, NonReservedPeerMode, PROTOCOL_VERSION}; +use {NetworkProtocolHandler, NonReservedPeerMode, AllowIP, PROTOCOL_VERSION}; use node_table::*; use stats::NetworkStats; use discovery::{Discovery, TableUpdates, NodeEntry}; @@ -45,8 +45,7 @@ use parking_lot::{Mutex, RwLock}; type Slab<T> = ::slab::Slab<T, usize>; const MAX_SESSIONS: usize = 1024 + MAX_HANDSHAKES; -const MAX_HANDSHAKES: usize = 80; -const MAX_HANDSHAKES_PER_ROUND: usize = 32; +const MAX_HANDSHAKES: usize = 1024; // Tokens const TCP_ACCEPT: usize = SYS_TIMER + 1; @@ -89,12 +88,18 @@ pub struct NetworkConfiguration { pub use_secret: Option<Secret>, /// Minimum number of connected peers to maintain pub min_peers: u32, - /// Maximum allowd number of peers + /// Maximum allowed number of peers pub max_peers: u32, + /// Maximum handshakes + pub max_handshakes: u32, + /// Reserved protocols. Peers with <key> protocol get additional <value> connection slots. + pub reserved_protocols: HashMap<ProtocolId, u32>, /// List of reserved node addresses. pub reserved_nodes: Vec<String>, /// The non-reserved peer mode. pub non_reserved_mode: NonReservedPeerMode, + /// IP filter + pub allow_ips: AllowIP, } impl Default for NetworkConfiguration { @@ -118,6 +123,9 @@ impl NetworkConfiguration { use_secret: None, min_peers: 25, max_peers: 50, + max_handshakes: 64, + reserved_protocols: HashMap::new(), + allow_ips: AllowIP::All, reserved_nodes: Vec::new(), non_reserved_mode: NonReservedPeerMode::Accept, } @@ -364,7 +372,7 @@ pub struct Host { impl Host { /// Create a new instance - pub fn new(config: NetworkConfiguration, stats: Arc<NetworkStats>) -> Result<Host, NetworkError> { + pub fn new(mut config: NetworkConfiguration, stats: Arc<NetworkStats>) -> Result<Host, NetworkError> { trace!(target: "host", "Creating new Host object"); let mut listen_address = match config.listen_address { @@ -394,6 +402,7 @@ impl Host { let boot_nodes = config.boot_nodes.clone(); let reserved_nodes = config.reserved_nodes.clone(); + config.max_handshakes = min(config.max_handshakes, MAX_HANDSHAKES as u32); let mut host = Host { info: RwLock::new(HostInfo { @@ -532,6 +541,7 @@ impl Host { } let local_endpoint = self.info.read().local_endpoint.clone(); let public_address = self.info.read().config.public_address.clone(); + let allow_ips = self.info.read().config.allow_ips; let public_endpoint = match public_address { None => { let public_address = select_public_address(local_endpoint.address.port()); @@ -563,7 +573,7 @@ impl Host { if info.config.discovery_enabled && info.config.non_reserved_mode == NonReservedPeerMode::Accept { let mut udp_addr = local_endpoint.address.clone(); udp_addr.set_port(local_endpoint.udp_port); - Some(Discovery::new(&info.keys, udp_addr, public_endpoint, DISCOVERY)) + Some(Discovery::new(&info.keys, udp_addr, public_endpoint, DISCOVERY, allow_ips)) } else { None } }; @@ -618,14 +628,14 @@ impl Host { } fn connect_peers(&self, io: &IoContext<NetworkIoMessage>) { - let (min_peers, mut pin) = { + let (min_peers, mut pin, max_handshakes, allow_ips) = { let info = self.info.read(); if info.capabilities.is_empty() { return; } let config = &info.config; - (config.min_peers, config.non_reserved_mode == NonReservedPeerMode::Deny) + (config.min_peers, config.non_reserved_mode == NonReservedPeerMode::Deny, config.max_handshakes as usize, config.allow_ips) }; let session_count = self.session_count(); @@ -642,22 +652,22 @@ impl Host { let handshake_count = self.handshake_count(); // allow 16 slots for incoming connections - let handshake_limit = MAX_HANDSHAKES - 16; - if handshake_count >= handshake_limit { + if handshake_count >= max_handshakes { return; } // iterate over all nodes, reserved ones coming first. // if we are pinned to only reserved nodes, ignore all others. let nodes = reserved_nodes.iter().cloned().chain(if !pin { - self.nodes.read().nodes() + self.nodes.read().nodes(allow_ips) } else { Vec::new() }); + let max_handshakes_per_round = max_handshakes / 2; let mut started: usize = 0; for id in nodes.filter(|ref id| !self.have_session(id) && !self.connecting_to(id)) - .take(min(MAX_HANDSHAKES_PER_ROUND, handshake_limit - handshake_count)) { + .take(min(max_handshakes_per_round, max_handshakes - handshake_count)) { self.connect_peer(&id, io); started += 1; } @@ -790,7 +800,14 @@ impl Host { let session_count = self.session_count(); let (max_peers, reserved_only) = { let info = self.info.read(); - (info.config.max_peers, info.config.non_reserved_mode == NonReservedPeerMode::Deny) + let mut max_peers = info.config.max_peers; + for cap in s.info.capabilities.iter() { + if let Some(num) = info.config.reserved_protocols.get(&cap.protocol) { + max_peers += *num; + break; + } + } + (max_peers, info.config.non_reserved_mode == NonReservedPeerMode::Deny) }; if session_count >= max_peers as usize || reserved_only { diff --git a/util/network/src/ip_utils.rs b/util/network/src/ip_utils.rs index 9bdafb7c7..7ccf75200 100644 --- a/util/network/src/ip_utils.rs +++ b/util/network/src/ip_utils.rs @@ -56,6 +56,22 @@ impl SocketAddrExt for Ipv6Addr { } } +impl SocketAddrExt for IpAddr { + fn is_unspecified_s(&self) -> bool { + match *self { + IpAddr::V4(ref ip) => ip.is_unspecified_s(), + IpAddr::V6(ref ip) => ip.is_unspecified_s(), + } + } + + fn is_global_s(&self) -> bool { + match *self { + IpAddr::V4(ref ip) => ip.is_global_s(), + IpAddr::V6(ref ip) => ip.is_global_s(), + } + } +} + #[cfg(not(windows))] mod getinterfaces { use std::{mem, io, ptr}; diff --git a/util/network/src/lib.rs b/util/network/src/lib.rs index 627458c1c..fcd36235a 100644 --- a/util/network/src/lib.rs +++ b/util/network/src/lib.rs @@ -137,3 +137,15 @@ impl NonReservedPeerMode { } } } + +/// IP fiter +#[derive(Clone, Debug, PartialEq, Eq, Copy)] +pub enum AllowIP { + /// Connect to any address + All, + /// Connect to private network only + Private, + /// Connect to public network only + Public, +} + diff --git a/util/network/src/node_table.rs b/util/network/src/node_table.rs index c90e35a27..97fb29607 100644 --- a/util/network/src/node_table.rs +++ b/util/network/src/node_table.rs @@ -30,6 +30,7 @@ use util::UtilError; use rlp::*; use time::Tm; use error::NetworkError; +use AllowIP; use discovery::{TableUpdates, NodeEntry}; use ip_utils::*; pub use rustc_serialize::json::Json; @@ -53,9 +54,15 @@ impl NodeEndpoint { SocketAddr::V6(a) => SocketAddr::V6(SocketAddrV6::new(a.ip().clone(), self.udp_port, a.flowinfo(), a.scope_id())), } } -} -impl NodeEndpoint { + pub fn is_allowed(&self, filter: AllowIP) -> bool { + match filter { + AllowIP::All => true, + AllowIP::Private => !self.address.ip().is_global_s(), + AllowIP::Public => self.address.ip().is_global_s(), + } + } + pub fn from_rlp(rlp: &UntrustedRlp) -> Result<Self, DecoderError> { let tcp_port = try!(rlp.val_at::<u16>(2)); let udp_port = try!(rlp.val_at::<u16>(1)); @@ -98,13 +105,6 @@ impl NodeEndpoint { SocketAddr::V6(a) => !a.ip().is_unspecified_s() } } - - pub fn is_global(&self) -> bool { - match self.address { - SocketAddr::V4(a) => a.ip().is_global_s(), - SocketAddr::V6(a) => a.ip().is_global_s() - } - } } impl FromStr for NodeEndpoint { @@ -219,8 +219,8 @@ impl NodeTable { } /// Returns node ids sorted by number of failures - pub fn nodes(&self) -> Vec<NodeId> { - let mut refs: Vec<&Node> = self.nodes.values().filter(|n| !self.useless_nodes.contains(&n.id)).collect(); + pub fn nodes(&self, filter: AllowIP) -> Vec<NodeId> { + let mut refs: Vec<&Node> = self.nodes.values().filter(|n| !self.useless_nodes.contains(&n.id) && n.endpoint.is_allowed(filter)).collect(); refs.sort_by(|a, b| a.failures.cmp(&b.failures)); refs.iter().map(|n| n.id.clone()).collect() } @@ -278,7 +278,7 @@ impl NodeTable { let mut json = String::new(); json.push_str("{\n"); json.push_str("\"nodes\": [\n"); - let node_ids = self.nodes(); + let node_ids = self.nodes(AllowIP::All); for i in 0 .. node_ids.len() { let node = self.nodes.get(&node_ids[i]).unwrap(); json.push_str(&format!("\t{{ \"url\": \"{}\", \"failures\": {} }}{}\n", node, node.failures, if i == node_ids.len() - 1 {""} else {","})) @@ -361,6 +361,7 @@ mod tests { use std::net::*; use util::hash::*; use devtools::*; + use AllowIP; #[test] fn endpoint_parse() { @@ -406,7 +407,7 @@ mod tests { table.note_failure(&id1); table.note_failure(&id2); - let r = table.nodes(); + let r = table.nodes(AllowIP::All); assert_eq!(r[0][..], id3[..]); assert_eq!(r[1][..], id2[..]); assert_eq!(r[2][..], id1[..]); @@ -428,7 +429,7 @@ mod tests { { let table = NodeTable::new(Some(temp_path.as_path().to_str().unwrap().to_owned())); - let r = table.nodes(); + let r = table.nodes(AllowIP::All); assert_eq!(r[0][..], id1[..]); assert_eq!(r[1][..], id2[..]); } diff --git a/util/src/kvdb.rs b/util/src/kvdb.rs index 4c5b7a3d0..3a2652c3d 100644 --- a/util/src/kvdb.rs +++ b/util/src/kvdb.rs @@ -608,7 +608,6 @@ mod tests { use super::*; use devtools::*; use std::str::FromStr; - use std::path::PathBuf; fn test_db(config: &DatabaseConfig) { let path = RandomTempPath::create_dir(); @@ -673,6 +672,7 @@ mod tests { #[test] #[cfg(target_os = "linux")] fn df_to_rotational() { + use std::path::PathBuf; // Example df output. let example_df = vec![70, 105, 108, 101, 115, 121, 115, 116, 101, 109, 32, 32, 32, 32, 32, 49, 75, 45, 98, 108, 111, 99, 107, 115, 32, 32, 32, 32, 32, 85, 115, 101, 100, 32, 65, 118, 97, 105, 108, 97, 98, 108, 101, 32, 85, 115, 101, 37, 32, 77, 111, 117, 110, 116, 101, 100, 32, 111, 110, 10, 47, 100, 101, 118, 47, 115, 100, 97, 49, 32, 32, 32, 32, 32, 32, 32, 54, 49, 52, 48, 57, 51, 48, 48, 32, 51, 56, 56, 50, 50, 50, 51, 54, 32, 32, 49, 57, 52, 52, 52, 54, 49, 54, 32, 32, 54, 55, 37, 32, 47, 10]; let expected_output = Some(PathBuf::from("/sys/block/sda/queue/rotational")); From bc81ae04077ef3158c7e7ad607fee6e9a12c95b3 Mon Sep 17 00:00:00 2001 From: Robert Habermeier <rphmeier@gmail.com> Date: Mon, 24 Oct 2016 18:27:23 +0200 Subject: [PATCH 29/77] Snapshot and blockchain stability improvements (#2843) * allow taking snapshot from just-restored database without error * make creation informant less spammy * Ancestry iterator failure-resilient * make uncle hash searching resilient to incomplete chain * deduce pre-chunk info from last written block's details --- ethcore/src/blockchain/blockchain.rs | 35 +++++++++++----- ethcore/src/snapshot/mod.rs | 62 +++++++++++++--------------- ethcore/src/snapshot/tests/blocks.rs | 2 +- parity/informant.rs | 23 ++++++----- parity/snapshot.rs | 5 +-- 5 files changed, 68 insertions(+), 59 deletions(-) diff --git a/ethcore/src/blockchain/blockchain.rs b/ethcore/src/blockchain/blockchain.rs index 799fa383d..de0c72a38 100644 --- a/ethcore/src/blockchain/blockchain.rs +++ b/ethcore/src/blockchain/blockchain.rs @@ -394,6 +394,8 @@ impl BlockProvider for BlockChain { } } +/// An iterator which walks the blockchain towards the genesis. +#[derive(Clone)] pub struct AncestryIter<'a> { current: H256, chain: &'a BlockChain, @@ -403,11 +405,10 @@ impl<'a> Iterator for AncestryIter<'a> { type Item = H256; fn next(&mut self) -> Option<H256> { if self.current.is_zero() { - Option::None + None } else { - let mut n = self.chain.block_details(&self.current).unwrap().parent; - mem::swap(&mut self.current, &mut n); - Some(n) + self.chain.block_details(&self.current) + .map(|details| mem::replace(&mut self.current, details.parent)) } } } @@ -999,17 +1000,29 @@ impl BlockChain { if !self.is_known(parent) { return None; } let mut excluded = HashSet::new(); - for a in self.ancestry_iter(parent.clone()).unwrap().take(uncle_generations) { - excluded.extend(self.uncle_hashes(&a).unwrap().into_iter()); - excluded.insert(a); + let ancestry = match self.ancestry_iter(parent.clone()) { + Some(iter) => iter, + None => return None, + }; + + for a in ancestry.clone().take(uncle_generations) { + if let Some(uncles) = self.uncle_hashes(&a) { + excluded.extend(uncles); + excluded.insert(a); + } else { + break + } } let mut ret = Vec::new(); - for a in self.ancestry_iter(parent.clone()).unwrap().skip(1).take(uncle_generations) { - ret.extend(self.block_details(&a).unwrap().children.iter() - .filter(|h| !excluded.contains(h)) - ); + for a in ancestry.skip(1).take(uncle_generations) { + if let Some(details) = self.block_details(&a) { + ret.extend(details.children.iter().filter(|h| !excluded.contains(h))) + } else { + break + } } + Some(ret) } diff --git a/ethcore/src/snapshot/mod.rs b/ethcore/src/snapshot/mod.rs index 927c27424..4523f4f16 100644 --- a/ethcore/src/snapshot/mod.rs +++ b/ethcore/src/snapshot/mod.rs @@ -136,7 +136,7 @@ pub fn take_snapshot<W: SnapshotWriter + Send>( let writer = Mutex::new(writer); let (state_hashes, block_hashes) = try!(scope(|scope| { - let block_guard = scope.spawn(|| chunk_blocks(chain, (number, block_at), &writer, p)); + let block_guard = scope.spawn(|| chunk_blocks(chain, block_at, &writer, p)); let state_res = chunk_state(state_db, state_root, &writer, p); state_res.and_then(|state_hashes| { @@ -176,10 +176,15 @@ struct BlockChunker<'a> { impl<'a> BlockChunker<'a> { // Repeatedly fill the buffers and writes out chunks, moving backwards from starting block hash. // Loops until we reach the first desired block, and writes out the remainder. - fn chunk_all(&mut self, first_hash: H256) -> Result<(), Error> { + fn chunk_all(&mut self) -> Result<(), Error> { let mut loaded_size = 0; + let mut last = self.current_hash; + + let genesis_hash = self.chain.genesis_hash(); + + for _ in 0..SNAPSHOT_BLOCKS { + if self.current_hash == genesis_hash { break } - while self.current_hash != first_hash { let (block, receipts) = try!(self.chain.block(&self.current_hash) .and_then(|b| self.chain.block_receipts(&self.current_hash).map(|r| (b, r))) .ok_or(Error::BlockNotFound(self.current_hash))); @@ -197,21 +202,21 @@ impl<'a> BlockChunker<'a> { // cut off the chunk if too large. - if new_loaded_size > PREFERRED_CHUNK_SIZE { - try!(self.write_chunk()); + if new_loaded_size > PREFERRED_CHUNK_SIZE && self.rlps.len() > 0 { + try!(self.write_chunk(last)); loaded_size = pair.len(); } else { loaded_size = new_loaded_size; } self.rlps.push_front(pair); + + last = self.current_hash; self.current_hash = view.header_view().parent_hash(); } if loaded_size != 0 { - // we don't store the first block, so once we get to this point, - // the "first" block will be first_number + 1. - try!(self.write_chunk()); + try!(self.write_chunk(last)); } Ok(()) @@ -219,23 +224,24 @@ impl<'a> BlockChunker<'a> { // write out the data in the buffers to a chunk on disk // - // we preface each chunk with the parent of the first block's details. - fn write_chunk(&mut self) -> Result<(), Error> { - // since the block we're inspecting now doesn't go into the - // chunk if it's too large, the current hash is the parent hash - // for the first block in that chunk. - let parent_hash = self.current_hash; - + // we preface each chunk with the parent of the first block's details, + // obtained from the details of the last block written. + fn write_chunk(&mut self, last: H256) -> Result<(), Error> { trace!(target: "snapshot", "prepared block chunk with {} blocks", self.rlps.len()); - let (parent_number, parent_details) = try!(self.chain.block_number(&parent_hash) - .and_then(|n| self.chain.block_details(&parent_hash).map(|d| (n, d))) - .ok_or(Error::BlockNotFound(parent_hash))); - let parent_total_difficulty = parent_details.total_difficulty; + let (last_header, last_details) = try!(self.chain.block_header(&last) + .and_then(|n| self.chain.block_details(&last).map(|d| (n, d))) + .ok_or(Error::BlockNotFound(last))); + + let parent_number = last_header.number() - 1; + let parent_hash = last_header.parent_hash(); + let parent_total_difficulty = last_details.total_difficulty - *last_header.difficulty(); + + trace!(target: "snapshot", "parent last written block: {}", parent_hash); let num_entries = self.rlps.len(); let mut rlp_stream = RlpStream::new_list(3 + num_entries); - rlp_stream.append(&parent_number).append(&parent_hash).append(&parent_total_difficulty); + rlp_stream.append(&parent_number).append(parent_hash).append(&parent_total_difficulty); for pair in self.rlps.drain(..) { rlp_stream.append_raw(&pair, 1); @@ -264,17 +270,7 @@ impl<'a> BlockChunker<'a> { /// The path parameter is the directory to store the block chunks in. /// This function assumes the directory exists already. /// Returns a list of chunk hashes, with the first having the blocks furthest from the genesis. -pub fn chunk_blocks<'a>(chain: &'a BlockChain, start_block_info: (u64, H256), writer: &Mutex<SnapshotWriter + 'a>, progress: &'a Progress) -> Result<Vec<H256>, Error> { - let (start_number, start_hash) = start_block_info; - - let first_hash = if start_number < SNAPSHOT_BLOCKS { - // use the genesis hash. - chain.genesis_hash() - } else { - let first_num = start_number - SNAPSHOT_BLOCKS; - try!(chain.block_hash(first_num).ok_or(Error::IncompleteChain)) - }; - +pub fn chunk_blocks<'a>(chain: &'a BlockChain, start_hash: H256, writer: &Mutex<SnapshotWriter + 'a>, progress: &'a Progress) -> Result<Vec<H256>, Error> { let mut chunker = BlockChunker { chain: chain, rlps: VecDeque::new(), @@ -285,7 +281,7 @@ pub fn chunk_blocks<'a>(chain: &'a BlockChain, start_block_info: (u64, H256), wr progress: progress, }; - try!(chunker.chunk_all(first_hash)); + try!(chunker.chunk_all()); Ok(chunker.hashes) } @@ -596,7 +592,7 @@ impl BlockRebuilder { let rlp = UntrustedRlp::new(chunk); let item_count = rlp.item_count(); - trace!(target: "snapshot", "restoring block chunk with {} blocks.", item_count - 2); + trace!(target: "snapshot", "restoring block chunk with {} blocks.", item_count - 3); // todo: assert here that these values are consistent with chunks being in order. let mut cur_number = try!(rlp.val_at::<u64>(0)) + 1; diff --git a/ethcore/src/snapshot/tests/blocks.rs b/ethcore/src/snapshot/tests/blocks.rs index 06e069655..62c6ea2fe 100644 --- a/ethcore/src/snapshot/tests/blocks.rs +++ b/ethcore/src/snapshot/tests/blocks.rs @@ -57,7 +57,7 @@ fn chunk_and_restore(amount: u64) { // snapshot it. let writer = Mutex::new(PackedWriter::new(&snapshot_path).unwrap()); - let block_hashes = chunk_blocks(&bc, (amount, best_hash), &writer, &Progress::default()).unwrap(); + let block_hashes = chunk_blocks(&bc, best_hash, &writer, &Progress::default()).unwrap(); writer.into_inner().finish(::snapshot::ManifestData { state_hashes: Vec::new(), block_hashes: block_hashes, diff --git a/parity/informant.rs b/parity/informant.rs index 9d1679615..33ad54b3d 100644 --- a/parity/informant.rs +++ b/parity/informant.rs @@ -45,6 +45,14 @@ pub struct Informant { skipped: AtomicUsize, } +/// Format byte counts to standard denominations. +pub fn format_bytes(b: usize) -> String { + match binary_prefix(b as f64) { + Standalone(bytes) => format!("{} bytes", bytes), + Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix), + } +} + /// Something that can be converted to milliseconds. pub trait MillisecondDuration { /// Get the value in milliseconds. @@ -75,13 +83,6 @@ impl Informant { } } - fn format_bytes(b: usize) -> String { - match binary_prefix(b as f64) { - Standalone(bytes) => format!("{} bytes", bytes), - Prefixed(prefix, n) => format!("{:.0} {}B", n, prefix), - } - } - #[cfg_attr(feature="dev", allow(match_bool))] pub fn tick(&self) { @@ -156,11 +157,11 @@ impl Informant { _ => String::new(), }, format!("{} db {} chain {} queue{}", - paint(Blue.bold(), format!("{:>8}", Informant::format_bytes(report.state_db_mem))), - paint(Blue.bold(), format!("{:>8}", Informant::format_bytes(cache_info.total()))), - paint(Blue.bold(), format!("{:>8}", Informant::format_bytes(queue_info.mem_used))), + paint(Blue.bold(), format!("{:>8}", format_bytes(report.state_db_mem))), + paint(Blue.bold(), format!("{:>8}", format_bytes(cache_info.total()))), + paint(Blue.bold(), format!("{:>8}", format_bytes(queue_info.mem_used))), match sync_status { - Some(ref sync_info) => format!(" {} sync", paint(Blue.bold(), format!("{:>8}", Informant::format_bytes(sync_info.mem_used)))), + Some(ref sync_info) => format!(" {} sync", paint(Blue.bold(), format!("{:>8}", format_bytes(sync_info.mem_used)))), _ => String::new(), } ) diff --git a/parity/snapshot.rs b/parity/snapshot.rs index e5c3c672c..4cd972230 100644 --- a/parity/snapshot.rs +++ b/parity/snapshot.rs @@ -232,9 +232,8 @@ impl SnapshotCommand { let cur_size = p.size(); if cur_size != last_size { last_size = cur_size; - info!("Snapshot: {} accounts {} blocks {} bytes", p.accounts(), p.blocks(), p.size()); - } else { - info!("Snapshot: No progress since last update."); + let bytes = ::informant::format_bytes(p.size()); + info!("Snapshot: {} accounts {} blocks {}", p.accounts(), p.blocks(), bytes); } ::std::thread::sleep(Duration::from_secs(5)); From 1b42e9a9af7e13381cd35344e2fa1e22ce9b36fa Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac <ngotchac@gmail.com> Date: Mon, 24 Oct 2016 18:27:35 +0200 Subject: [PATCH 30/77] Added Export Component in Actionbar => export Addressbook (#2153) (#2847) --- js/package.json | 1 + js/src/ui/Actionbar/Export/export.js | 57 ++++++++++++++++++++++++++++ js/src/ui/Actionbar/Export/index.js | 17 +++++++++ js/src/ui/index.js | 2 + js/src/views/Addresses/addresses.js | 9 ++++- 5 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 js/src/ui/Actionbar/Export/export.js create mode 100644 js/src/ui/Actionbar/Export/index.js diff --git a/js/package.json b/js/package.json index 9bfcfb9be..d84b21d25 100644 --- a/js/package.json +++ b/js/package.json @@ -109,6 +109,7 @@ "blockies": "0.0.2", "bytes": "^2.4.0", "es6-promise": "^3.2.1", + "file-saver": "^1.3.3", "format-json": "^1.0.3", "format-number": "^2.0.1", "geopattern": "^1.2.3", diff --git a/js/src/ui/Actionbar/Export/export.js b/js/src/ui/Actionbar/Export/export.js new file mode 100644 index 000000000..64e153734 --- /dev/null +++ b/js/src/ui/Actionbar/Export/export.js @@ -0,0 +1,57 @@ +// 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 FileSaver from 'file-saver'; +import FileDownloadIcon from 'material-ui/svg-icons/file/file-download'; + +import { Button } from '../../'; + +class ActionbarExport extends Component { + static propTypes = { + content: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object + ]).isRequired, + filename: PropTypes.string.isRequired, + className: PropTypes.string + } + + render () { + const { className } = this.props; + + return ( + <Button + className={ className } + icon={ <FileDownloadIcon /> } + label='export' + onClick={ this.onDownloadBackup } /> + ); + } + + onDownloadBackup = () => { + const { filename, content } = this.props; + + const text = (typeof content === 'string') + ? content + : JSON.stringify(content, null, 4); + + const blob = new Blob([ text ], { type: 'text/plain;charset=utf-8' }); + FileSaver.saveAs(blob, filename); + } +} + +export default ActionbarExport; diff --git a/js/src/ui/Actionbar/Export/index.js b/js/src/ui/Actionbar/Export/index.js new file mode 100644 index 000000000..f4ac7092e --- /dev/null +++ b/js/src/ui/Actionbar/Export/index.js @@ -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 './export'; diff --git a/js/src/ui/index.js b/js/src/ui/index.js index 8c7117a5f..72ad058e0 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -15,6 +15,7 @@ // along with Parity. If not, see <http://www.gnu.org/licenses/>. import Actionbar from './Actionbar'; +import ActionbarExport from './Actionbar/Export'; import ActionbarSearch from './Actionbar/Search'; import ActionbarSort from './Actionbar/Sort'; import Badge from './Badge'; @@ -39,6 +40,7 @@ import TxHash from './TxHash'; export { Actionbar, + ActionbarExport, ActionbarSearch, ActionbarSort, AddressSelect, diff --git a/js/src/views/Addresses/addresses.js b/js/src/views/Addresses/addresses.js index 2ddb53881..13c512713 100644 --- a/js/src/views/Addresses/addresses.js +++ b/js/src/views/Addresses/addresses.js @@ -22,7 +22,7 @@ import { uniq } from 'lodash'; import List from '../Accounts/List'; import { AddAddress } from '../../modals'; -import { Actionbar, ActionbarSearch, ActionbarSort, Button, Page } from '../../ui'; +import { Actionbar, ActionbarExport, ActionbarSearch, ActionbarSort, Button, Page } from '../../ui'; import styles from './addresses.css'; @@ -93,6 +93,8 @@ class Addresses extends Component { } renderActionbar () { + const { contacts } = this.props; + const buttons = [ <Button key='newAddress' @@ -100,6 +102,11 @@ class Addresses extends Component { label='new address' onClick={ this.onOpenAdd } />, + <ActionbarExport + key='exportAddressbook' + content={ contacts } + filename='addressbook.json' />, + this.renderSearchButton(), this.renderSortButton() ]; From edbd6676963d134641da0982ef7caa0e9f7b51e3 Mon Sep 17 00:00:00 2001 From: Arkadiy Paronyan <arkady.paronyan@gmail.com> Date: Mon, 24 Oct 2016 18:32:06 +0200 Subject: [PATCH 31/77] Prevent database corruption on OOM (#2832) * Prevent database corruption on OOM * Renamed write_flushing --- util/src/kvdb.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/util/src/kvdb.rs b/util/src/kvdb.rs index 3a2652c3d..63d46d573 100644 --- a/util/src/kvdb.rs +++ b/util/src/kvdb.rs @@ -277,7 +277,8 @@ pub struct Database { // Values currently being flushed. Cleared when `flush` completes. flushing: RwLock<Vec<HashMap<ElasticArray32<u8>, KeyState>>>, // Prevents concurrent flushes. - flushing_lock: Mutex<()>, + // Value indicates if a flush is in progress. + flushing_lock: Mutex<bool>, } impl Database { @@ -379,7 +380,7 @@ impl Database { write_opts: write_opts, overlay: RwLock::new((0..(num_cols + 1)).map(|_| HashMap::new()).collect()), flushing: RwLock::new((0..(num_cols + 1)).map(|_| HashMap::new()).collect()), - flushing_lock: Mutex::new(()), + flushing_lock: Mutex::new((false)), path: path.to_owned(), read_opts: read_opts, }) @@ -417,11 +418,10 @@ impl Database { }; } - /// Commit buffered changes to database. - pub fn flush(&self) -> Result<(), String> { + /// Commit buffered changes to database. Must be called under `flush_lock` + fn write_flushing_with_lock(&self, _lock: &mut MutexGuard<bool>) -> Result<(), String> { match *self.db.read() { Some(DBAndColumns { ref db, ref cfs }) => { - let _lock = self.flushing_lock.lock(); let batch = WriteBatch::new(); mem::swap(&mut *self.overlay.write(), &mut *self.flushing.write()); { @@ -464,6 +464,20 @@ impl Database { } } + /// Commit buffered changes to database. + pub fn flush(&self) -> Result<(), String> { + let mut lock = self.flushing_lock.lock(); + // If RocksDB batch allocation fails the thread gets terminated and the lock is released. + // The value inside the lock is used to detect that. + if *lock { + // This can only happen if another flushing thread is terminated unexpectedly. + return Err("Database write failure. Running low on memory perhaps?".to_owned()); + } + *lock = true; + let result = self.write_flushing_with_lock(&mut lock); + *lock = false; + result + } /// Commit transaction to database. pub fn write(&self, tr: DBTransaction) -> Result<(), String> { From 0fedc2733228e7b3c67fc6dc5b542352463fd38d Mon Sep 17 00:00:00 2001 From: Robert Habermeier <rphmeier@gmail.com> Date: Mon, 24 Oct 2016 18:35:25 +0200 Subject: [PATCH 32/77] Remove ethcore::common re-export module (#2792) * no longer export action_params * remove transaction, header, receipt re-rexports from common * remove env_info and builtins re-exports from common * remove everything but util export from common * replace common usages with util, remove module * add a prelude module for ethcore-bigint --- ethcore/src/action_params.rs | 6 +++++- ethcore/src/basic_types.rs | 2 +- ethcore/src/block.rs | 9 +++++++- ethcore/src/common.rs | 27 ------------------------ ethcore/src/engines/basic_authority.rs | 13 ++++++++++-- ethcore/src/engines/instant_seal.rs | 7 +++--- ethcore/src/engines/mod.rs | 7 +++++- ethcore/src/ethereum/ethash.rs | 16 ++++++++++---- ethcore/src/ethereum/mod.rs | 3 ++- ethcore/src/evm/benches/mod.rs | 3 ++- ethcore/src/evm/interpreter/gasometer.rs | 18 ++++++++-------- ethcore/src/evm/interpreter/mod.rs | 6 ++++-- ethcore/src/evm/jit.rs | 2 +- ethcore/src/evm/tests.rs | 4 +++- ethcore/src/executive.rs | 13 ++++++++++-- ethcore/src/externalities.rs | 9 ++++++-- ethcore/src/header.rs | 2 +- ethcore/src/json_tests/executive.rs | 2 ++ ethcore/src/json_tests/state.rs | 1 + ethcore/src/json_tests/test_common.rs | 2 +- ethcore/src/json_tests/transaction.rs | 1 + ethcore/src/lib.rs | 1 - ethcore/src/pod_account.rs | 2 +- ethcore/src/pod_state.rs | 2 +- ethcore/src/spec/spec.rs | 4 +++- ethcore/src/state/mod.rs | 8 +++++-- ethcore/src/tests/client.rs | 3 ++- ethcore/src/tests/helpers.rs | 10 ++++++--- ethcore/src/verification/verification.rs | 6 +++++- util/bigint/src/lib.rs | 13 ++++++++++++ 30 files changed, 130 insertions(+), 72 deletions(-) delete mode 100644 ethcore/src/common.rs diff --git a/ethcore/src/action_params.rs b/ethcore/src/action_params.rs index 46c159269..8b863c625 100644 --- a/ethcore/src/action_params.rs +++ b/ethcore/src/action_params.rs @@ -15,10 +15,14 @@ // along with Parity. If not, see <http://www.gnu.org/licenses/>. //! Evm input params. -use common::*; +use util::{Address, Bytes, Uint, U256}; +use util::hash::{H256, FixedHash}; +use util::sha3::{Hashable, SHA3_EMPTY}; use ethjson; use types::executed::CallType; +use std::sync::Arc; + /// Transaction value #[derive(Clone, Debug)] pub enum ActionValue { diff --git a/ethcore/src/basic_types.rs b/ethcore/src/basic_types.rs index 5f6515c0d..79f009fd1 100644 --- a/ethcore/src/basic_types.rs +++ b/ethcore/src/basic_types.rs @@ -16,7 +16,7 @@ //! Ethcore basic typenames. -use util::*; +use util::hash::H2048; /// Type for a 2048-bit log-bloom, as used by our blocks. pub type LogBloom = H2048; diff --git a/ethcore/src/block.rs b/ethcore/src/block.rs index 6645bf492..5d7305b91 100644 --- a/ethcore/src/block.rs +++ b/ethcore/src/block.rs @@ -587,10 +587,17 @@ pub fn enact_verified( mod tests { use tests::helpers::*; use super::*; - use common::*; use engines::Engine; + use env_info::LastHashes; + use error::Error; + use header::Header; use factory::Factories; use state_db::StateDB; + use views::BlockView; + use util::Address; + use util::hash::FixedHash; + + use std::sync::Arc; /// Enact the block given by `block_bytes` using `engine` on the database `db` with given `parent` block header #[cfg_attr(feature="dev", allow(too_many_arguments))] diff --git a/ethcore/src/common.rs b/ethcore/src/common.rs deleted file mode 100644 index 41fdd5397..000000000 --- a/ethcore/src/common.rs +++ /dev/null @@ -1,27 +0,0 @@ -// 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 use util::*; -pub use basic_types::*; -pub use error::*; -pub use env_info::*; -pub use views::*; -pub use builtin::*; -pub use header::*; -pub use transaction::*; -pub use log_entry::*; -pub use receipt::*; -pub use action_params::*; diff --git a/ethcore/src/engines/basic_authority.rs b/ethcore/src/engines/basic_authority.rs index bd3eb5bc6..815d2b43a 100644 --- a/ethcore/src/engines/basic_authority.rs +++ b/ethcore/src/engines/basic_authority.rs @@ -16,14 +16,20 @@ //! A blockchain engine that supports a basic, non-BFT proof-of-authority. -use common::*; use ethkey::{recover, public_to_address}; use account_provider::AccountProvider; use block::*; +use builtin::Builtin; use spec::CommonParams; use engines::Engine; +use env_info::EnvInfo; +use error::{BlockError, Error}; use evm::Schedule; use ethjson; +use header::Header; +use transaction::SignedTransaction; + +use util::*; /// `BasicAuthority` params. #[derive(Debug, PartialEq)] @@ -184,10 +190,13 @@ impl Header { #[cfg(test)] mod tests { - use common::*; + use util::*; use block::*; + use env_info::EnvInfo; + use error::{BlockError, Error}; use tests::helpers::*; use account_provider::AccountProvider; + use header::Header; use spec::Spec; /// Create a new test chain spec with `BasicAuthority` consensus engine. diff --git a/ethcore/src/engines/instant_seal.rs b/ethcore/src/engines/instant_seal.rs index 174a80ea8..acead19b4 100644 --- a/ethcore/src/engines/instant_seal.rs +++ b/ethcore/src/engines/instant_seal.rs @@ -18,11 +18,11 @@ use std::collections::BTreeMap; use util::Address; use builtin::Builtin; use engines::Engine; +use env_info::EnvInfo; use spec::CommonParams; use evm::Schedule; -use env_info::EnvInfo; use block::ExecutedBlock; -use common::Bytes; +use util::Bytes; use account_provider::AccountProvider; /// An engine which does not provide any consensus mechanism, just seals blocks internally. @@ -67,10 +67,11 @@ impl Engine for InstantSeal { #[cfg(test)] mod tests { - use common::*; + use util::*; use tests::helpers::*; use account_provider::AccountProvider; use spec::Spec; + use header::Header; use block::*; #[test] diff --git a/ethcore/src/engines/mod.rs b/ethcore/src/engines/mod.rs index e6325957a..250529dad 100644 --- a/ethcore/src/engines/mod.rs +++ b/ethcore/src/engines/mod.rs @@ -24,11 +24,16 @@ pub use self::null_engine::NullEngine; pub use self::instant_seal::InstantSeal; pub use self::basic_authority::BasicAuthority; -use common::*; +use util::*; use account_provider::AccountProvider; use block::ExecutedBlock; +use builtin::Builtin; +use env_info::EnvInfo; +use error::Error; use spec::CommonParams; use evm::Schedule; +use header::Header; +use transaction::SignedTransaction; /// A consensus mechanism for the chain. Generally either proof-of-work or proof-of-stake-based. /// Provides hooks into each of the major parts of block import. diff --git a/ethcore/src/ethereum/ethash.rs b/ethcore/src/ethereum/ethash.rs index e0c18292b..060a20aa2 100644 --- a/ethcore/src/ethereum/ethash.rs +++ b/ethcore/src/ethereum/ethash.rs @@ -15,9 +15,14 @@ // along with Parity. If not, see <http://www.gnu.org/licenses/>. use ethash::{quick_get_difficulty, slow_get_seedhash, EthashManager, H256 as EH256}; -use common::*; +use util::*; use block::*; +use builtin::Builtin; +use env_info::EnvInfo; +use error::{BlockError, Error}; +use header::Header; use spec::CommonParams; +use transaction::SignedTransaction; use engines::Engine; use evm::Schedule; use ethjson; @@ -187,8 +192,8 @@ impl Engine for Ethash { // Commit state so that we can actually figure out the state root. if let Err(e) = fields.state.commit() { - warn!("Encountered error on state commit: {}", e); - } + warn!("Encountered error on state commit: {}", e); + } } fn verify_block_basic(&self, header: &Header, _block: Option<&[u8]>) -> result::Result<(), Error> { @@ -371,9 +376,12 @@ impl Header { #[cfg(test)] mod tests { - use common::*; + use util::*; use block::*; use tests::helpers::*; + use env_info::EnvInfo; + use error::{BlockError, Error}; + use header::Header; use super::super::new_morden; use super::Ethash; use rlp; diff --git a/ethcore/src/ethereum/mod.rs b/ethcore/src/ethereum/mod.rs index 219b3bf5c..d8299324d 100644 --- a/ethcore/src/ethereum/mod.rs +++ b/ethcore/src/ethereum/mod.rs @@ -65,10 +65,11 @@ pub fn new_morden() -> Spec { load(include_bytes!("../../res/ethereum/morden.jso #[cfg(test)] mod tests { - use common::*; + use util::*; use state::*; use super::*; use tests::helpers::*; + use views::BlockView; #[test] fn ensure_db_good() { diff --git a/ethcore/src/evm/benches/mod.rs b/ethcore/src/evm/benches/mod.rs index 8ef730d88..cbed7d881 100644 --- a/ethcore/src/evm/benches/mod.rs +++ b/ethcore/src/evm/benches/mod.rs @@ -24,7 +24,8 @@ extern crate test; use self::test::{Bencher, black_box}; -use common::*; +use util::*; +use action_params::ActionParams; use evm::{self, Factory, VMType}; use evm::tests::FakeExt; diff --git a/ethcore/src/evm/interpreter/gasometer.rs b/ethcore/src/evm/interpreter/gasometer.rs index d4c329be0..3fde3f664 100644 --- a/ethcore/src/evm/interpreter/gasometer.rs +++ b/ethcore/src/evm/interpreter/gasometer.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. -use common::*; +use util::*; use super::u256_to_address; use evm::{self, CostType}; use evm::instructions::{self, Instruction, InstructionInfo}; @@ -64,14 +64,14 @@ impl<Gas: CostType> Gasometer<Gas> { Some(cap_divisor) if self.current_gas >= needed => { let gas_remaining = self.current_gas - needed; let max_gas_provided = gas_remaining - gas_remaining / Gas::from(cap_divisor); - if let Some(Ok(r)) = requested { + if let Some(Ok(r)) = requested { Ok(min(r, max_gas_provided)) } else { Ok(max_gas_provided) } }, _ => { - if let Some(r) = requested { + if let Some(r) = requested { r } else if self.current_gas >= needed { Ok(self.current_gas - needed) @@ -84,10 +84,10 @@ impl<Gas: CostType> Gasometer<Gas> { #[cfg_attr(feature="dev", allow(cyclomatic_complexity))] /// Determine how much gas is used by the given instruction, given the machine's state. - /// + /// /// We guarantee that the final element of the returned tuple (`provided`) will be `Some` /// iff the `instruction` is one of `CREATE`, or any of the `CALL` variants. In this case, - /// it will be the amount of gas that the current context provides to the child context. + /// it will be the amount of gas that the current context provides to the child context. pub fn get_gas_cost_mem( &mut self, ext: &evm::Ext, @@ -183,7 +183,7 @@ impl<Gas: CostType> Gasometer<Gas> { gas = overflowing!(gas.overflow_add(schedule.call_value_transfer_gas.into())); }; - // TODO: refactor to avoid duplicate calculation here and later on. + // TODO: refactor to avoid duplicate calculation here and later on. let (mem_gas_cost, _, _) = try!(self.mem_gas_cost(schedule, current_mem_size, &mem)); let cost_so_far = overflowing!(gas.overflow_add(mem_gas_cost.into())); let requested = Gas::from_u256(*stack.peek(0)); @@ -199,7 +199,7 @@ impl<Gas: CostType> Gasometer<Gas> { try!(mem_needed(stack.peek(2), stack.peek(3))) ); - // TODO: refactor to avoid duplicate calculation here and later on. + // TODO: refactor to avoid duplicate calculation here and later on. let (mem_gas_cost, _, _) = try!(self.mem_gas_cost(schedule, current_mem_size, &mem)); let cost_so_far = overflowing!(gas.overflow_add(mem_gas_cost.into())); let requested = Gas::from_u256(*stack.peek(0)); @@ -212,9 +212,9 @@ impl<Gas: CostType> Gasometer<Gas> { let mut gas = Gas::from(schedule.create_gas); let mem = try!(mem_needed(stack.peek(1), stack.peek(2))); - // TODO: refactor to avoid duplicate calculation here and later on. + // TODO: refactor to avoid duplicate calculation here and later on. let (mem_gas_cost, _, _) = try!(self.mem_gas_cost(schedule, current_mem_size, &mem)); - let cost_so_far = overflowing!(gas.overflow_add(mem_gas_cost.into())); + let cost_so_far = overflowing!(gas.overflow_add(mem_gas_cost.into())); let provided = try!(self.gas_provided(schedule, cost_so_far, None)); gas = overflowing!(gas.overflow_add(provided)); diff --git a/ethcore/src/evm/interpreter/mod.rs b/ethcore/src/evm/interpreter/mod.rs index f9d386a21..a39e09e79 100644 --- a/ethcore/src/evm/interpreter/mod.rs +++ b/ethcore/src/evm/interpreter/mod.rs @@ -29,12 +29,14 @@ use self::memory::Memory; pub use self::shared_cache::SharedCache; use std::marker::PhantomData; -use common::*; +use action_params::{ActionParams, ActionValue}; use types::executed::CallType; -use super::instructions::{self, Instruction, InstructionInfo}; +use evm::instructions::{self, Instruction, InstructionInfo}; use evm::{self, MessageCallResult, ContractCreateResult, GasLeft, CostType}; use bit_set::BitSet; +use util::*; + type CodePosition = usize; type ProgramCounter = usize; diff --git a/ethcore/src/evm/jit.rs b/ethcore/src/evm/jit.rs index 16a7b29e5..6fa617396 100644 --- a/ethcore/src/evm/jit.rs +++ b/ethcore/src/evm/jit.rs @@ -15,7 +15,7 @@ // along with Parity. If not, see <http://www.gnu.org/licenses/>. //! Just in time compiler execution environment. -use common::*; +use util::*; use evmjit; use evm::{self, GasLeft}; use types::executed::CallType; diff --git a/ethcore/src/evm/tests.rs b/ethcore/src/evm/tests.rs index f685e279d..ba002d649 100644 --- a/ethcore/src/evm/tests.rs +++ b/ethcore/src/evm/tests.rs @@ -14,7 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. -use common::*; +use util::*; +use action_params::{ActionParams, ActionValue}; +use env_info::EnvInfo; use types::executed::CallType; use evm::{self, Ext, Schedule, Factory, GasLeft, VMType, ContractCreateResult, MessageCallResult}; use std::fmt::Debug; diff --git a/ethcore/src/executive.rs b/ethcore/src/executive.rs index f93424415..f05cc4fd8 100644 --- a/ethcore/src/executive.rs +++ b/ethcore/src/executive.rs @@ -15,13 +15,17 @@ // along with Parity. If not, see <http://www.gnu.org/licenses/>. //! Transaction Execution environment. -use common::*; +use util::*; +use action_params::{ActionParams, ActionValue}; use state::{State, Substate}; use engines::Engine; use types::executed::CallType; +use env_info::EnvInfo; +use error::ExecutionError; use evm::{self, Ext, Factory, Finalize}; use externalities::*; use trace::{FlatTrace, Tracer, NoopTracer, ExecutiveTracer, VMTrace, VMTracer, ExecutiveVMTracer, NoopVMTracer}; +use transaction::{Action, SignedTransaction}; use crossbeam; pub use types::executed::{Executed, ExecutionResult}; @@ -500,13 +504,18 @@ impl<'a> Executive<'a> { mod tests { use ethkey::{Generator, Random}; use super::*; - use common::*; + use util::*; + use action_params::{ActionParams, ActionValue}; + use env_info::EnvInfo; use evm::{Factory, VMType}; + use error::ExecutionError; use state::Substate; use tests::helpers::*; use trace::trace; use trace::{FlatTrace, Tracer, NoopTracer, ExecutiveTracer}; use trace::{VMTrace, VMOperation, VMExecutedOperation, MemoryDiff, StorageDiff, VMTracer, NoopVMTracer, ExecutiveVMTracer}; + use transaction::{Action, Transaction}; + use types::executed::CallType; #[test] diff --git a/ethcore/src/externalities.rs b/ethcore/src/externalities.rs index 67c04aefb..bbe81a511 100644 --- a/ethcore/src/externalities.rs +++ b/ethcore/src/externalities.rs @@ -15,9 +15,11 @@ // along with Parity. If not, see <http://www.gnu.org/licenses/>. //! Transaction Execution environment. -use common::*; +use util::*; +use action_params::{ActionParams, ActionValue}; use state::{State, Substate}; use engines::Engine; +use env_info::EnvInfo; use executive::*; use evm::{self, Schedule, Ext, ContractCreateResult, MessageCallResult, Factory}; use types::executed::CallType; @@ -253,6 +255,8 @@ impl<'a, T, V> Ext for Externalities<'a, T, V> where T: 'a + Tracer, V: 'a + VMT } fn log(&mut self, topics: Vec<H256>, data: &[u8]) { + use log_entry::LogEntry; + let address = self.origin_info.address.clone(); self.substate.logs.push(LogEntry { address: address, @@ -303,8 +307,9 @@ impl<'a, T, V> Ext for Externalities<'a, T, V> where T: 'a + Tracer, V: 'a + VMT #[cfg(test)] mod tests { - use common::*; + use util::*; use engines::Engine; + use env_info::EnvInfo; use evm::Ext; use state::{State, Substate}; use tests::helpers::*; diff --git a/ethcore/src/header.rs b/ethcore/src/header.rs index 50f1f573c..228933570 100644 --- a/ethcore/src/header.rs +++ b/ethcore/src/header.rs @@ -17,7 +17,7 @@ //! Block header. use util::*; -use basic_types::*; +use basic_types::{LogBloom, Seal, ZERO_LOGBLOOM}; use time::get_time; use rlp::*; diff --git a/ethcore/src/json_tests/executive.rs b/ethcore/src/json_tests/executive.rs index 8979b8253..1d4faec62 100644 --- a/ethcore/src/json_tests/executive.rs +++ b/ethcore/src/json_tests/executive.rs @@ -15,9 +15,11 @@ // along with Parity. If not, see <http://www.gnu.org/licenses/>. use super::test_common::*; +use action_params::ActionParams; use state::{State, Substate}; use executive::*; use engines::Engine; +use env_info::EnvInfo; use evm; use evm::{Schedule, Ext, Factory, Finalize, VMType, ContractCreateResult, MessageCallResult}; use externalities::*; diff --git a/ethcore/src/json_tests/state.rs b/ethcore/src/json_tests/state.rs index 16e532401..c3e74af5d 100644 --- a/ethcore/src/json_tests/state.rs +++ b/ethcore/src/json_tests/state.rs @@ -17,6 +17,7 @@ use super::test_common::*; use tests::helpers::*; use pod_state::{self, PodState}; +use log_entry::LogEntry; use ethereum; use ethjson; diff --git a/ethcore/src/json_tests/test_common.rs b/ethcore/src/json_tests/test_common.rs index 7f7051bf0..e77b3df93 100644 --- a/ethcore/src/json_tests/test_common.rs +++ b/ethcore/src/json_tests/test_common.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. -pub use common::*; +pub use util::*; macro_rules! test { ($name: expr) => { diff --git a/ethcore/src/json_tests/transaction.rs b/ethcore/src/json_tests/transaction.rs index a06e3b5dc..50061cbfd 100644 --- a/ethcore/src/json_tests/transaction.rs +++ b/ethcore/src/json_tests/transaction.rs @@ -18,6 +18,7 @@ use super::test_common::*; use evm; use ethjson; use rlp::{UntrustedRlp, View}; +use transaction::{Action, SignedTransaction}; fn do_json_test(json_data: &[u8]) -> Vec<String> { let tests = ethjson::transaction::Test::load(json_data).unwrap(); diff --git a/ethcore/src/lib.rs b/ethcore/src/lib.rs index c72a977cf..9985dc58e 100644 --- a/ethcore/src/lib.rs +++ b/ethcore/src/lib.rs @@ -140,7 +140,6 @@ pub mod db; mod cache_manager; mod blooms; -mod common; mod basic_types; mod env_info; mod pod_account; diff --git a/ethcore/src/pod_account.rs b/ethcore/src/pod_account.rs index a92e03ebc..afee32f94 100644 --- a/ethcore/src/pod_account.rs +++ b/ethcore/src/pod_account.rs @@ -163,7 +163,7 @@ pub fn diff_pod(pre: Option<&PodAccount>, post: Option<&PodAccount>) -> Option<A #[cfg(test)] mod test { - use common::*; + use util::*; use types::account_diff::*; use super::{PodAccount, diff_pod}; diff --git a/ethcore/src/pod_state.rs b/ethcore/src/pod_state.rs index 9a11fb33f..d9f44680e 100644 --- a/ethcore/src/pod_state.rs +++ b/ethcore/src/pod_state.rs @@ -77,7 +77,7 @@ pub fn diff_pod(pre: &PodState, post: &PodState) -> StateDiff { #[cfg(test)] mod test { - use common::*; + use util::*; use types::state_diff::*; use types::account_diff::*; use pod_account::PodAccount; diff --git a/ethcore/src/spec/spec.rs b/ethcore/src/spec/spec.rs index 694ec4be8..46e99c12e 100644 --- a/ethcore/src/spec/spec.rs +++ b/ethcore/src/spec/spec.rs @@ -16,10 +16,12 @@ //! Parameters for a block chain. -use common::*; +use util::*; +use builtin::Builtin; use engines::{Engine, NullEngine, InstantSeal, BasicAuthority}; use pod_state::*; use account_db::*; +use header::{BlockNumber, Header}; use state_db::StateDB; use super::genesis::Genesis; use super::seal::Generic as GenericSeal; diff --git a/ethcore/src/state/mod.rs b/ethcore/src/state/mod.rs index 02716b8de..2253ed89d 100644 --- a/ethcore/src/state/mod.rs +++ b/ethcore/src/state/mod.rs @@ -16,14 +16,18 @@ use std::cell::{RefCell, RefMut}; use std::collections::hash_map::Entry; -use common::*; +use util::*; +use receipt::Receipt; use engines::Engine; +use env_info::EnvInfo; +use error::Error; use executive::{Executive, TransactOptions}; use factory::Factories; use trace::FlatTrace; use pod_account::*; use pod_state::{self, PodState}; use types::state_diff::StateDiff; +use transaction::SignedTransaction; use state_db::StateDB; mod account; @@ -756,7 +760,7 @@ use super::*; use util::{U256, H256, FixedHash, Address, Hashable}; use tests::helpers::*; use devtools::*; -use env_info::*; +use env_info::EnvInfo; use spec::*; use transaction::*; use util::log::init_log; diff --git a/ethcore/src/tests/client.rs b/ethcore/src/tests/client.rs index 082cd3c78..e152ac37a 100644 --- a/ethcore/src/tests/client.rs +++ b/ethcore/src/tests/client.rs @@ -20,11 +20,12 @@ use ethereum; use block::IsBlock; use tests::helpers::*; use types::filter::Filter; -use common::*; +use util::*; use devtools::*; use miner::Miner; use rlp::{Rlp, View}; use spec::Spec; +use views::BlockView; #[test] fn imports_from_empty() { diff --git a/ethcore/src/tests/helpers.rs b/ethcore/src/tests/helpers.rs index 2fab04214..7b8264720 100644 --- a/ethcore/src/tests/helpers.rs +++ b/ethcore/src/tests/helpers.rs @@ -17,19 +17,23 @@ use ethkey::KeyPair; use io::*; use client::{BlockChainClient, Client, ClientConfig}; -use common::*; +use util::*; use spec::*; use state_db::StateDB; use block::{OpenBlock, Drain}; use blockchain::{BlockChain, Config as BlockChainConfig}; +use builtin::Builtin; use state::*; use evm::Schedule; use engines::Engine; +use env_info::EnvInfo; use ethereum; use devtools::*; use miner::Miner; +use header::Header; +use transaction::{Action, SignedTransaction, Transaction}; use rlp::{self, RlpStream, Stream}; -use db::COL_STATE; +use views::BlockView; #[cfg(feature = "json-tests")] pub enum ChainEra { @@ -346,7 +350,7 @@ pub fn get_temp_state() -> GuardedTempResult<State> { pub fn get_temp_state_db_in(path: &Path) -> StateDB { let db = new_db(path.to_str().expect("Only valid utf8 paths for tests.")); - let journal_db = journaldb::new(db.clone(), journaldb::Algorithm::EarlyMerge, COL_STATE); + let journal_db = journaldb::new(db.clone(), journaldb::Algorithm::EarlyMerge, ::db::COL_STATE); StateDB::new(journal_db, 5 * 1024 * 1024) } diff --git a/ethcore/src/verification/verification.rs b/ethcore/src/verification/verification.rs index eefed1261..1b8eddfe8 100644 --- a/ethcore/src/verification/verification.rs +++ b/ethcore/src/verification/verification.rs @@ -21,10 +21,14 @@ /// 2. Signatures verification done in the queue. /// 3. Final verification against the blockchain done before enactment. -use common::*; +use util::*; use engines::Engine; +use error::{BlockError, Error}; use blockchain::*; +use header::{BlockNumber, Header}; use rlp::{UntrustedRlp, View}; +use transaction::SignedTransaction; +use views::BlockView; /// Preprocessed block data gathered in `verify_block_unordered` call pub struct PreverifiedBlock { diff --git a/util/bigint/src/lib.rs b/util/bigint/src/lib.rs index 307aed3ce..0df69256c 100644 --- a/util/bigint/src/lib.rs +++ b/util/bigint/src/lib.rs @@ -24,3 +24,16 @@ extern crate rustc_serialize; pub mod uint; pub mod hash; + +/// A prelude module for re-exporting all the types defined in this crate. +/// +/// ```rust +/// use ethcore_bigint::prelude::*; +/// +/// let x: U256 = U256::zero(); +/// let y = x + 1.into(); +/// ``` +pub mod prelude { + pub use ::uint::*; + pub use ::hash::*; +} \ No newline at end of file From df23c9931c7c9419dfc7f6671c9d6a7ad90c908e Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" <general.beck@gmail.com> Date: Tue, 25 Oct 2016 03:31:22 +0700 Subject: [PATCH 33/77] Update gitlab-ci Add Global ENV RUSTFLAGS="-D warnings" --- .gitlab-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1022333fc..014aded1f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,6 +5,7 @@ variables: GIT_DEPTH: "3" SIMPLECOV: "true" RUST_BACKTRACE: "1" + RUSTFLAGS: "-D warnings" cache: key: "$CI_BUILD_NAME/$CI_BUILD_REF_NAME" untracked: true @@ -260,7 +261,7 @@ windows: - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Include;C:\vs2015\VC\include;C:\Program Files (x86)\Windows Kits\10\Include\10.0.10240.0\ucrt - set LIB=C:\vs2015\VC\lib;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10240.0\ucrt\x64 - set RUST_BACKTRACE=1 - - set RUSTFLAGS=%RUSTFLAGS% -Zorbit=off -D warnings + - set RUSTFLAGS="-Zorbit=off -D warnings" - rustup default stable-x86_64-pc-windows-msvc - git submodule update --init - cargo build --release --verbose From 97cdc2b4ac59f6f9e9be8ce054656cba2ab8a62c Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" <general.beck@gmail.com> Date: Tue, 25 Oct 2016 04:48:38 +0700 Subject: [PATCH 34/77] Update gitlab-ci fix RUSTFLAGS in win build --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 014aded1f..4eeb0644b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -261,7 +261,7 @@ windows: - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Include;C:\vs2015\VC\include;C:\Program Files (x86)\Windows Kits\10\Include\10.0.10240.0\ucrt - set LIB=C:\vs2015\VC\lib;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10240.0\ucrt\x64 - set RUST_BACKTRACE=1 - - set RUSTFLAGS="-Zorbit=off -D warnings" + - set RUSTFLAGS=-Zorbit=off - rustup default stable-x86_64-pc-windows-msvc - git submodule update --init - cargo build --release --verbose From cf67ed964e16e84e4c453bcd4df65164b53d3ee4 Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" <general.beck@gmail.com> Date: Tue, 25 Oct 2016 04:54:43 +0700 Subject: [PATCH 35/77] Update gitlab-ci fix windows RUSTFLAGS --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4eeb0644b..2a8c515f8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -262,6 +262,7 @@ windows: - set LIB=C:\vs2015\VC\lib;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10240.0\ucrt\x64 - set RUST_BACKTRACE=1 - set RUSTFLAGS=-Zorbit=off + - set RUSTFLAGS=-D warnings - rustup default stable-x86_64-pc-windows-msvc - git submodule update --init - cargo build --release --verbose From 285727e2fd26f7a53a932e105581d00e1c66ab27 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Tue, 25 Oct 2016 09:36:42 +0200 Subject: [PATCH 36/77] Auto-bump js-precompiled on release (#2828) * Auto-bump js-precompiled on release * [ci skip] js-precompiled 20161022-232245 * Remove old GitLab tests * Only move file.ext (get rid of error) * allow add, commit & push failures (auto-bump) --- .gitlab-ci.yml | 21 ++++++++++----------- js/scripts/build.sh | 1 + js/scripts/release.sh | 28 +++++++++++++++++++++------- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2a8c515f8..7a13531b3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ stages: - test variables: GIT_DEPTH: "3" - SIMPLECOV: "true" + SIMPLECOV: "true" RUST_BACKTRACE: "1" RUSTFLAGS: "-D warnings" cache: @@ -21,7 +21,7 @@ linux-stable: - cargo build --release --verbose - strip target/release/parity - md5sum target/release/parity >> parity.md5 - - aws configure set aws_access_key_id $s3_key + - aws configure set aws_access_key_id $s3_key - aws configure set aws_secret_access_key $s3_secret - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu/parity --body target/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu/parity.md5 --body parity.md5 @@ -44,7 +44,7 @@ linux-stable-14.04: - cargo build --release --verbose - strip target/release/parity - md5sum target/release/parity >> parity.md5 - - aws configure set aws_access_key_id $s3_key + - aws configure set aws_access_key_id $s3_key - aws configure set aws_secret_access_key $s3_secret - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-ubuntu_14_04-gnu/parity --body target/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-ubuntu_14_04-gnu/parity.md5 --body parity.md5 @@ -107,7 +107,7 @@ linux-centos: - cargo build --release --verbose - strip target/release/parity - md5sum target/release/parity >> parity.md5 - - aws configure set aws_access_key_id $s3_key + - aws configure set aws_access_key_id $s3_key - aws configure set aws_secret_access_key $s3_secret - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-centos-gnu/parity --body target/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-centos-gnu/parity.md5 --body parity.md5 @@ -134,7 +134,7 @@ linux-armv7: - cargo build --target armv7-unknown-linux-gnueabihf --release --verbose - arm-linux-gnueabihf-strip target/armv7-unknown-linux-gnueabihf/release/parity - md5sum target/armv7-unknown-linux-gnueabihf/release/parity >> parity.md5 - - aws configure set aws_access_key_id $s3_key + - aws configure set aws_access_key_id $s3_key - aws configure set aws_secret_access_key $s3_secret - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/armv7-unknown-linux-gnueabihf/parity --body target/armv7-unknown-linux-gnueabihf/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/armv7-unknown-linux-gnueabihf/parity.md5 --body parity.md5 @@ -162,7 +162,7 @@ linux-arm: - cargo build --target arm-unknown-linux-gnueabihf --release --verbose - arm-linux-gnueabihf-strip target/arm-unknown-linux-gnueabihf/release/parity - md5sum target/arm-unknown-linux-gnueabihf/release/parity >> parity.md5 - - aws configure set aws_access_key_id $s3_key + - aws configure set aws_access_key_id $s3_key - aws configure set aws_secret_access_key $s3_secret - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/arm-unknown-linux-gnueabihf/parity --body target/arm-unknown-linux-gnueabihf/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/arm-unknown-linux-gnueabihf/parity.md5 --body parity.md5 @@ -189,8 +189,8 @@ linux-armv6: - cat .cargo/config - cargo build --target arm-unknown-linux-gnueabi --release --verbose - arm-linux-gnueabi-strip target/arm-unknown-linux-gnueabi/release/parity - - md5sum target/arm-unknown-linux-gnueabi/release/parity >> parity.md5 - - aws configure set aws_access_key_id $s3_key + - md5sum target/arm-unknown-linux-gnueabi/release/parity >> parity.md5 + - aws configure set aws_access_key_id $s3_key - aws configure set aws_secret_access_key $s3_secret - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/arm-unknown-linux-gnueabi/parity --body target/arm-unknown-linux-gnueabi/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/arm-unknown-linux-gnueabi/parity.md5 --body parity.md5 @@ -218,7 +218,7 @@ linux-aarch64: - cargo build --target aarch64-unknown-linux-gnu --release --verbose - aarch64-linux-gnu-strip target/aarch64-unknown-linux-gnu/release/parity - md5sum target/aarch64-unknown-linux-gnu/release/parity >> parity.md5 - - aws configure set aws_access_key_id $s3_key + - aws configure set aws_access_key_id $s3_key - aws configure set aws_secret_access_key $s3_secret - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/aarch64-unknown-linux-gnu/parity --body target/aarch64-unknown-linux-gnu/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/aarch64-unknown-linux-gnu/parity.md5 --body parity.md5 @@ -240,7 +240,7 @@ darwin: script: - cargo build --release --verbose - md5sum target/release/parity >> parity.md5 - - aws configure set aws_access_key_id $s3_key + - aws configure set aws_access_key_id $s3_key - aws configure set aws_secret_access_key $s3_secret - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-apple-darwin/parity --body target/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-apple-darwin/parity.md5 --body parity.md5 @@ -316,7 +316,6 @@ js-release: stage: build image: ethcore/javascript:latest only: - - js - master - beta - tags diff --git a/js/scripts/build.sh b/js/scripts/build.sh index 07487ffd5..1f3f44ceb 100755 --- a/js/scripts/build.sh +++ b/js/scripts/build.sh @@ -6,6 +6,7 @@ cd .. # run build (production) and store the exit code EXITCODE=0 +rm -rf .build npm run ci:build || EXITCODE=1 # back to root diff --git a/js/scripts/release.sh b/js/scripts/release.sh index bff5ea7d9..de4bdc374 100755 --- a/js/scripts/release.sh +++ b/js/scripts/release.sh @@ -1,6 +1,14 @@ #!/bin/bash set -e +# setup the git user defaults for the current repo +function setup_git_user { + git config push.default simple + git config merge.ours.driver true + git config user.email "jaco+gitlab@ethcore.io" + git config user.name "GitLab Build Bot" +} + # change into the build directory pushd `dirname $0` cd ../.build @@ -10,7 +18,7 @@ UTCDATE=`date -u "+%Y%m%d-%H%M%S"` # Create proper directory structure mkdir -p build -mv * build || true +mv *.* build mkdir -p src # Copy rust files @@ -22,13 +30,8 @@ cp ../src/lib.rs* ./src/ rm -rf ./.git git init -# our user details -git config push.default simple -git config merge.ours.driver true -git config user.email "jaco+gitlab@ethcore.io" -git config user.name "GitLab Build Bot" - # add local files and send it up +setup_git_user git remote add origin https://${GITHUB_JS_PRECOMPILED}:@github.com/ethcore/js-precompiled.git git fetch origin git checkout -b $CI_BUILD_REF_NAME @@ -40,5 +43,16 @@ git push origin $CI_BUILD_REF_NAME # back to root popd +# bump js-precompiled +cargo update -p parity-ui-precompiled + +# add to git and push +setup_git_user +git remote set-url origin https://${GITHUB_JS_PRECOMPILED}:@github.com/ethcore/parity.git +git fetch origin +git add . || true +git commit -m "[ci skip] js-precompiled $UTCDATE" || true +git push origin || true + # exit with exit code exit 0 From 9f6da3f829b55a39454704222a07ec98661ac3f6 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Tue, 25 Oct 2016 11:43:05 +0200 Subject: [PATCH 37/77] Don't return empty names as clickable titles (#2809) * Don't return empty names (Fixes #2786) * add trim in validation --- js/src/ui/IdentityName/identityName.js | 8 +++++--- js/src/util/validation.js | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/js/src/ui/IdentityName/identityName.js b/js/src/ui/IdentityName/identityName.js index ca56bcacf..11e7c7d96 100644 --- a/js/src/ui/IdentityName/identityName.js +++ b/js/src/ui/IdentityName/identityName.js @@ -18,6 +18,8 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; +const defaultName = 'UNNAMED'; + class IdentityName extends Component { static propTypes = { address: PropTypes.string, @@ -38,14 +40,14 @@ class IdentityName extends Component { } const addressFallback = shorten ? this.formatHash(address) : address; - const fallback = unknown ? 'UNNAMED' : addressFallback; + const fallback = unknown ? defaultName : addressFallback; const isUuid = hasAccount && account.name === account.uuid; const name = hasAccount && !isUuid - ? account.name.toUpperCase() + ? account.name.toUpperCase().trim() : fallback; return ( - <span>{ name }</span> + <span>{ name && name.length ? name : fallback }</span> ); } diff --git a/js/src/util/validation.js b/js/src/util/validation.js index 1ec7489f8..243217077 100644 --- a/js/src/util/validation.js +++ b/js/src/util/validation.js @@ -81,7 +81,7 @@ export function validateCode (code, api) { } export function validateName (name) { - const nameError = !name || name.length < 2 ? ERRORS.invalidName : null; + const nameError = !name || name.trim().length < 2 ? ERRORS.invalidName : null; return { name, From 037a8c7625d8810b38f15ab5817107137ac344f5 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac <ngotchac@gmail.com> Date: Tue, 25 Oct 2016 11:43:24 +0200 Subject: [PATCH 38/77] Removes event name in unsubscribe API + Tests (#2364) (#2844) --- js/src/api/api.js | 4 +- js/src/api/subscriptions/manager.js | 51 +++++++++++------------- js/src/api/subscriptions/manager.spec.js | 40 ++++++++++++++++++- js/src/ui/TxHash/txHash.js | 2 +- js/src/views/Contract/contract.js | 2 +- 5 files changed, 65 insertions(+), 34 deletions(-) diff --git a/js/src/api/api.js b/js/src/api/api.js index c82129772..9768b9acb 100644 --- a/js/src/api/api.js +++ b/js/src/api/api.js @@ -90,8 +90,8 @@ export default class Api { return this._subscriptions.subscribe(subscriptionName, callback); } - unsubscribe (subscriptionName, subscriptionId) { - return this._subscriptions.unsubscribe(subscriptionName, subscriptionId); + unsubscribe (subscriptionId) { + return this._subscriptions.unsubscribe(subscriptionId); } pollMethod (method, input, validate) { diff --git a/js/src/api/subscriptions/manager.js b/js/src/api/subscriptions/manager.js index 6f9b7cf7e..61e06499e 100644 --- a/js/src/api/subscriptions/manager.js +++ b/js/src/api/subscriptions/manager.js @@ -29,17 +29,14 @@ const events = { 'personal_requestsToConfirm': { module: 'signer' } }; -let nextSubscriptionId = 0; - export default class Manager { constructor (api) { this._api = api; - this.subscriptions = {}; + this.subscriptions = []; this.values = {}; Object.keys(events).forEach((subscriptionName) => { - this.subscriptions[subscriptionName] = {}; this.values[subscriptionName] = { error: null, data: null @@ -71,61 +68,59 @@ export default class Manager { return; } - const subscriptionId = nextSubscriptionId++; + const subscriptionId = this.subscriptions.length; const { error, data } = this.values[subscriptionName]; const engine = this[`_${subscription.module}`]; - this.subscriptions[subscriptionName][subscriptionId] = callback; + this.subscriptions[subscriptionId] = { + name: subscriptionName, + id: subscriptionId, + callback + }; if (!engine.isStarted) { engine.start(); } else { - this._sendData(subscriptionName, subscriptionId, callback, error, data); + this._sendData(subscriptionId, error, data); } resolve(subscriptionId); }); } - unsubscribe (subscriptionName, subscriptionId) { + unsubscribe (subscriptionId) { return new Promise((resolve, reject) => { - const subscription = this._validateType(subscriptionName); - - if (isError(subscription)) { - reject(subscription); + if (!this.subscriptions[subscriptionId]) { + reject(new Error(`Cannot find subscription ${subscriptionId}`)); return; } - if (!this.subscriptions[subscriptionName][subscriptionId]) { - reject(new Error(`Cannot find subscription ${subscriptionId} for type ${subscriptionName}`)); - return; - } - - delete this.subscriptions[subscriptionName][subscriptionId]; + delete this.subscriptions[subscriptionId]; resolve(); }); } - _sendData (subscriptionName, subscriptionId, callback, error, data) { + _sendData (subscriptionId, error, data) { + const { callback } = this.subscriptions[subscriptionId]; + try { callback(error, data); } catch (error) { - console.error(`Unable to update callback for ${subscriptionName}, subscriptionId ${subscriptionId}`, error); - this.unsubscribe(subscriptionName, subscriptionId); + console.error(`Unable to update callback for subscriptionId ${subscriptionId}`, error); + this.unsubscribe(subscriptionId); } } _updateSubscriptions = (subscriptionName, error, data) => { - if (!this.subscriptions[subscriptionName]) { - throw new Error(`Cannot find entry point for subscriptions of type ${subscriptionName}`); - } + const subscriptions = this.subscriptions + .filter(subscription => subscription.name === subscriptionName); this.values[subscriptionName] = { error, data }; - Object.keys(this.subscriptions[subscriptionName]).forEach((subscriptionId) => { - const callback = this.subscriptions[subscriptionName][subscriptionId]; - this._sendData(subscriptionName, subscriptionId, callback, error, data); - }); + subscriptions + .forEach((subscription) => { + this._sendData(subscription.id, error, data); + }); } } diff --git a/js/src/api/subscriptions/manager.spec.js b/js/src/api/subscriptions/manager.spec.js index 4b6b84b84..5e434efec 100644 --- a/js/src/api/subscriptions/manager.spec.js +++ b/js/src/api/subscriptions/manager.spec.js @@ -20,6 +20,7 @@ import Manager, { events } from './manager'; function newStub () { const start = () => manager._updateSubscriptions(manager.__test, null, 'test'); + const manager = new Manager({ transport: { isConnected: true @@ -53,7 +54,7 @@ describe('api/subscriptions/manager', () => { describe('constructor', () => { it('sets up the subscription types & defaults', () => { - expect(Object.keys(manager.subscriptions)).to.deep.equal(Object.keys(events)); + expect(manager.subscriptions).to.be.an.array; expect(Object.keys(manager.values)).to.deep.equal(Object.keys(events)); }); }); @@ -74,6 +75,7 @@ describe('api/subscriptions/manager', () => { manager.__test = eventName; cb = sinon.stub(); sinon.spy(engine, 'start'); + return manager .subscribe(eventName, cb) .then((_subscriptionId) => { @@ -86,7 +88,7 @@ describe('api/subscriptions/manager', () => { }); it('returns a subscriptionId', () => { - expect(subscriptionId).to.be.ok; + expect(subscriptionId).to.be.a.number; }); it('calls the subscription callback with updated values', () => { @@ -95,4 +97,38 @@ describe('api/subscriptions/manager', () => { }); }); }); + + describe('unsubscriptions', () => { + Object + .keys(events) + .filter((eventName) => eventName.indexOf('_') !== -1) + .forEach((eventName) => { + const { module } = events[eventName]; + let engine; + let cb; + + describe(eventName, () => { + beforeEach(() => { + engine = manager[`_${module}`]; + manager.__test = eventName; + cb = sinon.stub(); + sinon.spy(engine, 'start'); + + return manager + .subscribe(eventName, cb) + .then((_subscriptionId) => { + manager.unsubscribe(_subscriptionId); + }) + .then(() => { + manager._updateSubscriptions(manager.__test, null, 'test2'); + }); + }); + + it('does not call the callback after unsibscription', () => { + expect(cb).to.have.been.calledWith(null, 'test'); + expect(cb).to.not.have.been.calledWith(null, 'test2'); + }); + }); + }); + }); }); diff --git a/js/src/ui/TxHash/txHash.js b/js/src/ui/TxHash/txHash.js index 79714cf9b..7b2080463 100644 --- a/js/src/ui/TxHash/txHash.js +++ b/js/src/ui/TxHash/txHash.js @@ -50,7 +50,7 @@ class TxHash extends Component { const { api } = this.context; const { subscriptionId } = this.state; - api.unsubscribe('eth_blockNumber', subscriptionId); + api.unsubscribe(subscriptionId); } render () { diff --git a/js/src/views/Contract/contract.js b/js/src/views/Contract/contract.js index 7a1889c19..ec12c3ac9 100644 --- a/js/src/views/Contract/contract.js +++ b/js/src/views/Contract/contract.js @@ -87,7 +87,7 @@ class Contract extends Component { const { api } = this.context; const { subscriptionId, blockSubscriptionId, contract } = this.state; - api.unsubscribe('eth_blockNumber', blockSubscriptionId); + api.unsubscribe(blockSubscriptionId); contract.unsubscribe(subscriptionId); } From 1af40a3db3c4b7be20f2f0d839923a3118525703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= <tomusdrw@users.noreply.github.com> Date: Tue, 25 Oct 2016 12:11:56 +0200 Subject: [PATCH 39/77] Fixing UI compilation (#2853) --- js/Cargo.precompiled.toml | 2 +- js/build.rs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/js/Cargo.precompiled.toml b/js/Cargo.precompiled.toml index 38e0a3b62..b8175e4f7 100644 --- a/js/Cargo.precompiled.toml +++ b/js/Cargo.precompiled.toml @@ -7,7 +7,7 @@ authors = ["Ethcore <admin@ethcore.io>"] build = "build.rs" [features] -default = ["with-syntex"] +default = ["with-syntex", "use-precompiled-js"] use-precompiled-js = ["parity-dapps-glue/use-precompiled-js"] with-syntex = ["parity-dapps-glue/with-syntex"] diff --git a/js/build.rs b/js/build.rs index 0048b7d5a..82bf1ac93 100644 --- a/js/build.rs +++ b/js/build.rs @@ -17,10 +17,6 @@ extern crate parity_dapps_glue; fn main() { - // FIXME: Currently creates an issue when - // (a) always trying to build & - // (b) trying to install node_modules inside the build directory (no package.json) - // Uncomment the next line when fixed to perfection - // parity_dapps_glue::js::build(env!("CARGO_MANIFEST_DIR"), "build"); + parity_dapps_glue::js::build(env!("CARGO_MANIFEST_DIR"), "build"); parity_dapps_glue::generate(); } From 5ae87a64b1077fc718c8655afc465eef0a978825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= <tomusdrw@users.noreply.github.com> Date: Tue, 25 Oct 2016 12:12:09 +0200 Subject: [PATCH 40/77] Bumping ws-rs (#2851) --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 5d18a9074..dce80bdce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1879,7 +1879,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "ws" version = "0.5.2" -source = "git+https://github.com/ethcore/ws-rs.git?branch=mio-upstream-stable#e3d21c119350e753fdf4475b8cd88103b2280540" +source = "git+https://github.com/ethcore/ws-rs.git?branch=mio-upstream-stable#00bd2134b07b4bc8ea47b7f6c7afce16bbe34c8f" dependencies = [ "bytes 0.4.0-dev (git+https://github.com/carllerche/bytes)", "httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", From a6fd922ffbabd6d17c657b9cdafb234a5d372fcd Mon Sep 17 00:00:00 2001 From: Gav Wood <gavin@ethcore.io> Date: Tue, 25 Oct 2016 12:21:21 +0200 Subject: [PATCH 41/77] Don't fail badly when no transactions in last 100 blocks. (#2856) --- ethcore/src/client/traits.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/ethcore/src/client/traits.rs b/ethcore/src/client/traits.rs index a8face23b..0bc9e70fa 100644 --- a/ethcore/src/client/traits.rs +++ b/ethcore/src/client/traits.rs @@ -194,15 +194,20 @@ pub trait BlockChainClient : Sync + Send { fn gas_price_statistics(&self, sample_size: usize, distribution_size: usize) -> Result<Vec<U256>, ()> { let mut h = self.chain_info().best_block_hash; let mut corpus = Vec::new(); - for _ in 0..sample_size { - let block_bytes = self.block(BlockID::Hash(h)).expect("h is either the best_block_hash or an ancestor; qed"); - let block = BlockView::new(&block_bytes); - let header = block.header_view(); - if header.number() == 0 { - break; + while corpus.is_empty() { + for _ in 0..sample_size { + let block_bytes = self.block(BlockID::Hash(h)).expect("h is either the best_block_hash or an ancestor; qed"); + let block = BlockView::new(&block_bytes); + let header = block.header_view(); + if header.number() == 0 { + if corpus.is_empty() { + corpus.push(20_000_000_000u64.into()); // we have literally no information - it' as good a number as any. + } + break; + } + block.transaction_views().iter().foreach(|t| corpus.push(t.gas_price())); + h = header.parent_hash().clone(); } - block.transaction_views().iter().foreach(|t| corpus.push(t.gas_price())); - h = header.parent_hash().clone(); } corpus.sort(); let n = corpus.len(); From b6f26280182af9097437df76949a39d4a6f603ad Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Tue, 25 Oct 2016 13:22:27 +0200 Subject: [PATCH 42/77] Token sorting, zero-ETH transfer & token decimals (#2805) * Error trapping for decimals (Fixes #2799) * Sort tokens by tag (Closes #2789) * PR comments * Always display ETH * Recalculate in all cases (traps >available) --- js/src/modals/Transfer/Details/details.js | 12 ++++---- js/src/modals/Transfer/errors.js | 1 + js/src/modals/Transfer/transfer.js | 29 ++++++++++++++++-- js/src/redux/providers/balances.js | 36 +++++++++++++++-------- 4 files changed, 57 insertions(+), 21 deletions(-) diff --git a/js/src/modals/Transfer/Details/details.js b/js/src/modals/Transfer/Details/details.js index 55896f0c7..c00a1ee43 100644 --- a/js/src/modals/Transfer/Details/details.js +++ b/js/src/modals/Transfer/Details/details.js @@ -119,17 +119,19 @@ export default class Details extends Component { const { balance, images, tag } = this.props; const items = balance.tokens - .filter((token) => token.value.gt(0)) - .map((balance, idx) => { + .filter((token, index) => !index || token.value.gt(0)) + .map((balance, index) => { const token = balance.token; - const isEth = idx === 0; + const isEth = index === 0; const imagesrc = token.image || images[token.address] || imageUnknown; let value = 0; if (isEth) { value = api.util.fromWei(balance.value).toFormat(3); } else { - value = new BigNumber(balance.value).div(balance.token.format || 1).toFormat(3); + const format = balance.token.format || 1; + const decimals = format === 1 ? 0 : Math.min(3, Math.floor(format / 10)); + value = new BigNumber(balance.value).div(format).toFormat(decimals); } const label = ( @@ -165,7 +167,7 @@ export default class Details extends Component { ); } - onChangeToken = (event, idx, tag) => { + onChangeToken = (event, index, tag) => { this.props.onChange('tag', tag); } diff --git a/js/src/modals/Transfer/errors.js b/js/src/modals/Transfer/errors.js index cc6f9b365..a6456c785 100644 --- a/js/src/modals/Transfer/errors.js +++ b/js/src/modals/Transfer/errors.js @@ -18,6 +18,7 @@ const ERRORS = { requireRecipient: 'a recipient network address is required for the transaction', invalidAddress: 'the supplied address is an invalid network address', invalidAmount: 'the supplied amount should be a valid positive number', + invalidDecimals: 'the supplied amount exceeds the allowed decimals', largeAmount: 'the transaction total is higher than the available balance' }; diff --git a/js/src/modals/Transfer/transfer.js b/js/src/modals/Transfer/transfer.js index f14048203..2217d0e9c 100644 --- a/js/src/modals/Transfer/transfer.js +++ b/js/src/modals/Transfer/transfer.js @@ -297,6 +297,24 @@ export default class Transfer extends Component { return null; } + validateDecimals (num) { + const { balance } = this.props; + const { tag } = this.state; + + if (tag === 'ETH') { + return null; + } + + const token = balance.tokens.find((balance) => balance.token.tag === tag).token; + const s = new BigNumber(num).mul(token.format || 1).toString(); + + if (s.indexOf('.') !== -1) { + return ERRORS.invalidDecimals; + } + + return null; + } + _onUpdateGas (gas) { const gasError = this.validatePositiveNumber(gas); @@ -341,7 +359,11 @@ export default class Transfer extends Component { } _onUpdateValue (value) { - const valueError = this.validatePositiveNumber(value); + let valueError = this.validatePositiveNumber(value); + + if (!valueError) { + valueError = this.validateDecimals(value); + } this.setState({ value, @@ -407,7 +429,7 @@ export default class Transfer extends Component { to: token.address }, [ recipient, - new BigNumber(value).mul(token.format).toString() + new BigNumber(value).mul(token.format).toFixed(0) ]); } @@ -500,6 +522,7 @@ export default class Transfer extends Component { }) .catch((error) => { console.error('etimateGas', error); + this.recalculate(); }); } @@ -565,7 +588,7 @@ export default class Transfer extends Component { }, this.recalculate); }) .catch((error) => { - console.error('getDefaults', error); + console.warn('getDefaults', error); }); } diff --git a/js/src/redux/providers/balances.js b/js/src/redux/providers/balances.js index b88883fb7..38321e640 100644 --- a/js/src/redux/providers/balances.js +++ b/js/src/redux/providers/balances.js @@ -100,21 +100,31 @@ export default class Balances { }) .then(([_tokens, images]) => { const tokens = {}; - this._tokens = _tokens.map((_token, index) => { - const [address, tag, format, name] = _token; + this._tokens = _tokens + .map((_token, index) => { + const [address, tag, format, name] = _token; - const token = { - address, - name, - tag, - format: format.toString(), - contract: this._api.newContract(abis.eip20, address) - }; - tokens[address] = token; - this._store.dispatch(setAddressImage(address, images[index])); + const token = { + address, + name, + tag, + format: format.toString(), + contract: this._api.newContract(abis.eip20, address) + }; + tokens[address] = token; + this._store.dispatch(setAddressImage(address, images[index])); - return token; - }); + return token; + }) + .sort((a, b) => { + if (a.tag < b.tag) { + return -1; + } else if (a.tag > b.tag) { + return 1; + } + + return 0; + }); this._store.dispatch(getTokens(tokens)); this._retrieveBalances(); From 4fc1c5f42e424d86669898530f2ef22a19afeca1 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Tue, 25 Oct 2016 15:15:22 +0200 Subject: [PATCH 43/77] Remove personal_* calls from dapps (#2860) * remove accountsInfo & listAccounts * registry accounts selector works --- js/src/dapps/basiccoin/Application/application.js | 5 +++-- js/src/dapps/gavcoin/Application/application.js | 9 +++++---- js/src/dapps/gavcoin/Events/Event/event.js | 2 +- js/src/dapps/githubhint/services.js | 9 +++++---- js/src/dapps/registry/addresses/actions.js | 13 +++++-------- js/src/dapps/registry/ui/address.js | 4 ++-- js/src/dapps/signaturereg/services.js | 7 ++++--- js/src/dapps/tokenreg/Accounts/actions.js | 8 ++++---- js/src/dapps/tokenreg/Status/actions.js | 2 +- 9 files changed, 30 insertions(+), 29 deletions(-) diff --git a/js/src/dapps/basiccoin/Application/application.js b/js/src/dapps/basiccoin/Application/application.js index 1e268e720..d84085c98 100644 --- a/js/src/dapps/basiccoin/Application/application.js +++ b/js/src/dapps/basiccoin/Application/application.js @@ -16,7 +16,7 @@ import React, { Component, PropTypes } from 'react'; -import { api } from '../parity'; +// import { api } from '../parity'; import { attachInstances } from '../services'; import Header from './Header'; @@ -83,9 +83,10 @@ export default class Application extends Component { Promise .all([ attachInstances(), - api.personal.accountsInfo() + null // api.personal.accountsInfo() ]) .then(([{ managerInstance, registryInstance, tokenregInstance }, accountsInfo]) => { + accountsInfo = accountsInfo || {}; this.setState({ loading: false, managerInstance, diff --git a/js/src/dapps/gavcoin/Application/application.js b/js/src/dapps/gavcoin/Application/application.js index 04c5abe01..29c86c78d 100644 --- a/js/src/dapps/gavcoin/Application/application.js +++ b/js/src/dapps/gavcoin/Application/application.js @@ -205,11 +205,12 @@ export default class Application extends Component { return Promise .all([ registry.getAddress.call({}, [api.util.sha3('gavcoin'), 'A']), - api.personal.listAccounts(), - api.personal.accountsInfo() + api.eth.accounts(), + null // api.personal.accountsInfo() ]); }) .then(([address, addresses, infos]) => { + infos = infos || {}; console.log(`gavcoin was found at ${address}`); const contract = api.newContract(abis.gavcoin, address); @@ -220,11 +221,11 @@ export default class Application extends Component { contract, instance: contract.instance, accounts: addresses.map((address) => { - const info = infos[address]; + const info = infos[address] || {}; return { address, - name: info.name || 'Unnamed', + name: info.name, uuid: info.uuid }; }) diff --git a/js/src/dapps/gavcoin/Events/Event/event.js b/js/src/dapps/gavcoin/Events/Event/event.js index 4fb29a382..0b4094ac0 100644 --- a/js/src/dapps/gavcoin/Events/Event/event.js +++ b/js/src/dapps/gavcoin/Events/Event/event.js @@ -80,7 +80,7 @@ export default class Event extends Component { const { accounts } = this.context; const account = accounts.find((_account) => _account.address === address); - if (account) { + if (account && account.name) { return ( <div className={ styles.name }> { account.name } diff --git a/js/src/dapps/githubhint/services.js b/js/src/dapps/githubhint/services.js index ee198ff6d..1904be2d7 100644 --- a/js/src/dapps/githubhint/services.js +++ b/js/src/dapps/githubhint/services.js @@ -28,21 +28,22 @@ export function attachInterface () { return Promise .all([ registry.getAddress.call({}, [api.util.sha3('githubhint'), 'A']), - api.personal.listAccounts(), - api.personal.accountsInfo() + api.eth.accounts(), + null // api.personal.accountsInfo() ]); }) .then(([address, addresses, accountsInfo]) => { + accountsInfo = accountsInfo || {}; console.log(`githubhint was found at ${address}`); const contract = api.newContract(abis.githubhint, address); const accounts = addresses.reduce((obj, address) => { - const info = accountsInfo[address]; + const info = accountsInfo[address] || {}; return Object.assign(obj, { [address]: { address, - name: info.name || 'Unnamed', + name: info.name, uuid: info.uuid } }); diff --git a/js/src/dapps/registry/addresses/actions.js b/js/src/dapps/registry/addresses/actions.js index b6091acb5..dfd7d16a3 100644 --- a/js/src/dapps/registry/addresses/actions.js +++ b/js/src/dapps/registry/addresses/actions.js @@ -21,16 +21,13 @@ export const set = (addresses) => ({ type: 'addresses set', addresses }); export const fetch = () => (dispatch) => { return Promise .all([ - api.personal.listAccounts(), - api.personal.accountsInfo() + api.eth.accounts(), + null // api.personal.accountsInfo() ]) .then(([ accounts, data ]) => { - const addresses = Object.keys(data) - .filter((address) => data[address] && !data[address].meta.deleted) - .map((address) => ({ - ...data[address], address, - isAccount: accounts.includes(address) - })); + const addresses = accounts.map((address) => { + return { address, isAccount: true }; + }); dispatch(set(addresses)); }) .catch((error) => { diff --git a/js/src/dapps/registry/ui/address.js b/js/src/dapps/registry/ui/address.js index e4a5a953e..f0b9d65da 100644 --- a/js/src/dapps/registry/ui/address.js +++ b/js/src/dapps/registry/ui/address.js @@ -32,9 +32,9 @@ const align = { export default (address, accounts, contacts, shortenHash = true) => { let caption; if (accounts[address]) { - caption = (<abbr title={ address } style={ align }>{ accounts[address].name }</abbr>); + caption = (<abbr title={ address } style={ align }>{ accounts[address].name || address }</abbr>); } else if (contacts[address]) { - caption = (<abbr title={ address } style={ align }>{ contacts[address].name }</abbr>); + caption = (<abbr title={ address } style={ align }>{ contacts[address].name || address }</abbr>); } else { caption = (<code style={ align }>{ shortenHash ? renderHash(address) : address }</code>); } diff --git a/js/src/dapps/signaturereg/services.js b/js/src/dapps/signaturereg/services.js index 3963d394c..3942f75cc 100644 --- a/js/src/dapps/signaturereg/services.js +++ b/js/src/dapps/signaturereg/services.js @@ -46,16 +46,17 @@ export function attachInterface (callback) { return Promise .all([ registry.getAddress.call({}, [api.util.sha3('signaturereg'), 'A']), - api.personal.listAccounts(), - api.personal.accountsInfo() + api.eth.accounts(), + null // api.personal.accountsInfo() ]); }) .then(([address, addresses, accountsInfo]) => { + accountsInfo = accountsInfo || {}; console.log(`signaturereg was found at ${address}`); const contract = api.newContract(abis.signaturereg, address); const accounts = addresses.reduce((obj, address) => { - const info = accountsInfo[address]; + const info = accountsInfo[address] || {}; return Object.assign(obj, { [address]: { diff --git a/js/src/dapps/tokenreg/Accounts/actions.js b/js/src/dapps/tokenreg/Accounts/actions.js index 2e597f2e5..f093b5300 100644 --- a/js/src/dapps/tokenreg/Accounts/actions.js +++ b/js/src/dapps/tokenreg/Accounts/actions.js @@ -37,11 +37,11 @@ export const setSelectedAccount = (address) => ({ export const loadAccounts = () => (dispatch) => { Promise .all([ - api.personal.listAccounts(), - api.personal.accountsInfo() + api.eth.accounts(), + null // api.personal.accountsInfo() ]) - .then(results => { - const [ accounts, accountsInfo ] = results; + .then(([ accounts, accountsInfo ]) => { + accountsInfo = accountsInfo || {}; const accountsList = accounts .map(address => ({ diff --git a/js/src/dapps/tokenreg/Status/actions.js b/js/src/dapps/tokenreg/Status/actions.js index a479179b2..9ec196aed 100644 --- a/js/src/dapps/tokenreg/Status/actions.js +++ b/js/src/dapps/tokenreg/Status/actions.js @@ -82,7 +82,7 @@ export const loadContractDetails = () => (dispatch, getState) => { Promise .all([ - api.personal.listAccounts(), + api.eth.accounts(), instance.owner.call(), instance.fee.call() ]) From 8d0cff3599cf2f951811e91ed1aaa889e8ee02a2 Mon Sep 17 00:00:00 2001 From: keorn <pczaban@gmail.com> Date: Tue, 25 Oct 2016 14:55:53 +0100 Subject: [PATCH 44/77] Nicer port in use errors (#2859) * dapps port * rpc port * signer port * different instance as possible cause * network port --- parity/dapps.rs | 6 +++++- parity/rpc.rs | 6 +++++- parity/signer.rs | 5 ++++- sync/src/api.rs | 7 ++++++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/parity/dapps.rs b/parity/dapps.rs index 46ac1ec60..6ef64c2fd 100644 --- a/parity/dapps.rs +++ b/parity/dapps.rs @@ -102,6 +102,7 @@ mod server { use super::Dependencies; use std::sync::Arc; use std::net::SocketAddr; + use std::io; use util::{Bytes, Address, U256}; use ethcore::transaction::{Transaction, Action}; @@ -143,7 +144,10 @@ mod server { }; match start_result { - Err(dapps::ServerError::IoError(err)) => Err(format!("WebApps io error: {}", err)), + Err(dapps::ServerError::IoError(err)) => match err.kind() { + io::ErrorKind::AddrInUse => Err(format!("WebApps address {} is already in use, make sure that another instance of an Ethereum client is not running or change the address using the --dapps-port and --dapps-interface options.", url)), + _ => Err(format!("WebApps io error: {}", err)), + }, Err(e) => Err(format!("WebApps error: {:?}", e)), Ok(server) => { server.set_panic_handler(move || { diff --git a/parity/rpc.rs b/parity/rpc.rs index 5600a7f06..c5a4df18a 100644 --- a/parity/rpc.rs +++ b/parity/rpc.rs @@ -17,6 +17,7 @@ use std::fmt; use std::sync::Arc; use std::net::SocketAddr; +use std::io; use io::PanicHandler; use ethcore_rpc::{RpcServerError, RpcServer as Server}; use jsonipc; @@ -108,7 +109,10 @@ pub fn setup_http_rpc_server( let ph = dependencies.panic_handler.clone(); let start_result = server.start_http(url, cors_domains, allowed_hosts, ph); match start_result { - Err(RpcServerError::IoError(err)) => Err(format!("RPC io error: {}", err)), + Err(RpcServerError::IoError(err)) => match err.kind() { + io::ErrorKind::AddrInUse => Err(format!("RPC address {} is already in use, make sure that another instance of an Ethereum client is not running or change the address using the --jsonrpc-port and --jsonrpc-interface options.", url)), + _ => Err(format!("RPC io error: {}", err)), + }, Err(e) => Err(format!("RPC error: {:?}", e)), Ok(server) => Ok(server), } diff --git a/parity/signer.rs b/parity/signer.rs index 869c7fab5..5097192ad 100644 --- a/parity/signer.rs +++ b/parity/signer.rs @@ -103,7 +103,10 @@ fn do_start(conf: Configuration, deps: Dependencies) -> Result<SignerServer, Str }; match start_result { - Err(signer::ServerError::IoError(err)) => Err(format!("Trusted Signer Error: {}", err)), + Err(signer::ServerError::IoError(err)) => match err.kind() { + io::ErrorKind::AddrInUse => Err(format!("Trusted Signer address {} is already in use, make sure that another instance of an Ethereum client is not running or change the address using the --signer-port and --signer-interface options.", addr)), + _ => Err(format!("Trusted Signer io error: {}", err)), + }, Err(e) => Err(format!("Trusted Signer Error: {:?}", e)), Ok(server) => { deps.panic_handler.forward_from(&server); diff --git a/sync/src/api.rs b/sync/src/api.rs index fb8fdc691..b227dcd60 100644 --- a/sync/src/api.rs +++ b/sync/src/api.rs @@ -16,6 +16,7 @@ use std::sync::Arc; use std::collections::HashMap; +use std::io; use util::Bytes; use network::{NetworkProtocolHandler, NetworkService, NetworkContext, PeerId, ProtocolId, NetworkConfiguration as BasicNetworkConfiguration, NonReservedPeerMode, NetworkError, @@ -210,7 +211,11 @@ impl ChainNotify for EthSync { } fn start(&self) { - self.network.start().unwrap_or_else(|e| warn!("Error starting network: {:?}", e)); + match self.network.start() { + Err(NetworkError::StdIo(ref e)) if e.kind() == io::ErrorKind::AddrInUse => warn!("Network port {:?} is already in use, make sure that another instance of an Ethereum client is not running or change the port using the --port option.", self.network.config().listen_address.expect("Listen address is not set.")), + Err(err) => warn!("Error starting network: {}", err), + _ => {}, + } self.network.register_protocol(self.handler.clone(), self.subprotocol_name, ETH_PACKET_COUNT, &[62u8, 63u8]) .unwrap_or_else(|e| warn!("Error registering ethereum protocol: {:?}", e)); // register the warp sync subprotocol From 7eacf07629cbf0a48a21ff1127d05736ee29686b Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Tue, 25 Oct 2016 16:02:12 +0200 Subject: [PATCH 45/77] Align contract event log l&f with transactions (#2812) * Event into own component, align with transactions * Pass value & type through from event log params * Reformat display columns --- js/src/api/contract/contract.js | 4 +- .../dapps/basiccoin/Deploy/Events/events.js | 4 + .../dapps/basiccoin/Transfer/Events/events.js | 5 +- js/src/dapps/gavcoin/Events/events.js | 5 +- js/src/dapps/registry/Events/events.js | 8 +- js/src/dapps/signaturereg/services.js | 5 +- js/src/dapps/tokenreg/Status/actions.js | 16 +- js/src/views/Contract/Events/Event/event.js | 183 ++++++++++++++++++ js/src/views/Contract/Events/Event/index.js | 17 ++ js/src/views/Contract/Events/events.js | 91 +-------- js/src/views/Contract/contract.css | 42 +++- js/src/views/Contract/contract.js | 1 - 12 files changed, 272 insertions(+), 109 deletions(-) create mode 100644 js/src/views/Contract/Events/Event/event.js create mode 100644 js/src/views/Contract/Events/Event/index.js diff --git a/js/src/api/contract/contract.js b/js/src/api/contract/contract.js index 40caa7643..cef75eda7 100644 --- a/js/src/api/contract/contract.js +++ b/js/src/api/contract/contract.js @@ -150,7 +150,9 @@ export default class Contract { log.event = event.name; decoded.params.forEach((param) => { - log.params[param.name] = param.token.value; + const { type, value } = param.token; + + log.params[param.name] = { type, value }; }); return log; diff --git a/js/src/dapps/basiccoin/Deploy/Events/events.js b/js/src/dapps/basiccoin/Deploy/Events/events.js index a21672a8e..4b51afb59 100644 --- a/js/src/dapps/basiccoin/Deploy/Events/events.js +++ b/js/src/dapps/basiccoin/Deploy/Events/events.js @@ -110,6 +110,10 @@ export default class Events extends Component { logToEvent = (log) => { log.key = api.util.sha3(JSON.stringify(log)); + log.params = Object.keys(log.params).reduce((params, name) => { + params[name] = log.params[name].value; + return params; + }, {}); return log; } diff --git a/js/src/dapps/basiccoin/Transfer/Events/events.js b/js/src/dapps/basiccoin/Transfer/Events/events.js index dcead03bb..101c77f73 100644 --- a/js/src/dapps/basiccoin/Transfer/Events/events.js +++ b/js/src/dapps/basiccoin/Transfer/Events/events.js @@ -114,6 +114,10 @@ export default class Events extends Component { logToEvent = (log) => { log.key = api.util.sha3(JSON.stringify(log)); + log.params = Object.keys(log.params).reduce((params, name) => { + params[name] = log.params[name].value; + return params; + }, {}); return log; } @@ -139,7 +143,6 @@ export default class Events extends Component { .concat(pendingEvents) .filter((log) => !minedNew.find((event) => event.transactionHash === log.transactionHash)); const events = [].concat(pendingNew).concat(minedNew); - console.log('*** events', events.map((event) => event.address)); this.setState({ loading: false, events, minedEvents: minedNew, pendingEvents: pendingNew }); } } diff --git a/js/src/dapps/gavcoin/Events/events.js b/js/src/dapps/gavcoin/Events/events.js index cb287b3a7..ba71d6541 100644 --- a/js/src/dapps/gavcoin/Events/events.js +++ b/js/src/dapps/gavcoin/Events/events.js @@ -106,7 +106,10 @@ export default class Events extends Component { logIndex, transactionHash, transactionIndex, - params, + params: Object.keys(params).reduce((data, name) => { + data[name] = params[name].value; + return data; + }, {}), key }; }; diff --git a/js/src/dapps/registry/Events/events.js b/js/src/dapps/registry/Events/events.js index ffb1fc919..10280ae52 100644 --- a/js/src/dapps/registry/Events/events.js +++ b/js/src/dapps/registry/Events/events.js @@ -48,9 +48,9 @@ const renderEvent = (classNames, verb) => (e, accounts, contacts) => { return ( <tr key={ e.key } className={ classes }> - <td>{ renderAddress(e.parameters.owner, accounts, contacts) }</td> + <td>{ renderAddress(e.parameters.owner.value, accounts, contacts) }</td> <td><abbr title={ e.transaction }>{ verb }</abbr></td> - <td><code>{ renderHash(bytesToHex(e.parameters.name)) }</code></td> + <td><code>{ renderHash(bytesToHex(e.parameters.name.value)) }</code></td> <td>{ renderStatus(e.timestamp, e.state === 'pending') }</td> </tr> ); @@ -64,10 +64,10 @@ const renderDataChanged = (e, accounts, contacts) => { return ( <tr key={ e.key } className={ classNames }> - <td>{ renderAddress(e.parameters.owner, accounts, contacts) }</td> + <td>{ renderAddress(e.parameters.owner.value, accounts, contacts) }</td> <td><abbr title={ e.transaction }>updated</abbr></td> <td> - key <code>{ new Buffer(e.parameters.plainKey).toString('utf8') }</code> of <code>{ renderHash(bytesToHex(e.parameters.name)) }</code> + key <code>{ new Buffer(e.parameters.plainKey.value).toString('utf8') }</code> of <code>{ renderHash(bytesToHex(e.parameters.name.value)) }</code> </td> <td>{ renderStatus(e.timestamp, e.state === 'pending') }</td> </tr> diff --git a/js/src/dapps/signaturereg/services.js b/js/src/dapps/signaturereg/services.js index 3942f75cc..7219ddff1 100644 --- a/js/src/dapps/signaturereg/services.js +++ b/js/src/dapps/signaturereg/services.js @@ -30,7 +30,10 @@ const logToEvent = (log) => { logIndex, transactionHash, transactionIndex, - params, + params: Object.keys(params).reduce((data, name) => { + data[name] = params[name].value; + return data; + }, {}), key }; }; diff --git a/js/src/dapps/tokenreg/Status/actions.js b/js/src/dapps/tokenreg/Status/actions.js index 9ec196aed..b7de9c108 100644 --- a/js/src/dapps/tokenreg/Status/actions.js +++ b/js/src/dapps/tokenreg/Status/actions.js @@ -148,27 +148,27 @@ export const subscribeEvents = () => (dispatch, getState) => { return dispatch(setTokenData(params.id.toNumber(), { tla: '...', base: -1, - address: params.addr, - name: params.name, + address: params.addr.value, + name: params.name.value, isPending: true })); } if (event === 'Registered' && type === 'mined') { - return dispatch(loadToken(params.id.toNumber())); + return dispatch(loadToken(params.id.value.toNumber())); } if (event === 'Unregistered' && type === 'pending') { - return dispatch(setTokenPending(params.id.toNumber(), true)); + return dispatch(setTokenPending(params.id.value.toNumber(), true)); } if (event === 'Unregistered' && type === 'mined') { - return dispatch(deleteToken(params.id.toNumber())); + return dispatch(deleteToken(params.id.value.toNumber())); } if (event === 'MetaChanged' && type === 'pending') { return dispatch(setTokenData( - params.id.toNumber(), + params.id.value.toNumber(), { metaPending: true, metaMined: false } )); } @@ -176,13 +176,13 @@ export const subscribeEvents = () => (dispatch, getState) => { if (event === 'MetaChanged' && type === 'mined') { setTimeout(() => { dispatch(setTokenData( - params.id.toNumber(), + params.id.value.toNumber(), { metaPending: false, metaMined: false } )); }, 5000); return dispatch(setTokenData( - params.id.toNumber(), + params.id.value.toNumber(), { metaPending: false, metaMined: true } )); } diff --git a/js/src/views/Contract/Events/Event/event.js b/js/src/views/Contract/Events/Event/event.js new file mode 100644 index 000000000..cfe5be251 --- /dev/null +++ b/js/src/views/Contract/Events/Event/event.js @@ -0,0 +1,183 @@ +// 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 BigNumber from 'bignumber.js'; +import moment from 'moment'; +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import { fetchBlock, fetchTransaction } from '../../../../redux/providers/blockchainActions'; +import { IdentityIcon, IdentityName, Input, InputAddress } from '../../../../ui'; + +import styles from '../../contract.css'; + +class Event extends Component { + static contextTypes = { + api: PropTypes.object.isRequired + } + + static propTypes = { + event: PropTypes.object.isRequired, + blocks: PropTypes.object, + transactions: PropTypes.object, + isTest: PropTypes.bool, + fetchBlock: PropTypes.func.isRequired, + fetchTransaction: PropTypes.func.isRequired + } + + componentDidMount () { + this.retrieveTransaction(); + } + + render () { + const { event, blocks, transactions, isTest } = this.props; + + const block = blocks[event.blockNumber.toString()]; + const transaction = transactions[event.transactionHash] || {}; + const classes = `${styles.event} ${styles[event.state]}`; + const url = `https://${isTest ? 'testnet.' : ''}etherscan.io/tx/${event.transactionHash}`; + const keys = Object.keys(event.params).join(', '); + const values = Object.keys(event.params).map((name, index) => { + const param = event.params[name]; + + return ( + <div className={ styles.eventValue } key={ `${event.key}_val_${index}` }> + { this.renderParam(name, param) } + </div> + ); + }); + + return ( + <tr className={ classes }> + <td className={ styles.timestamp }> + <div>{ event.state === 'pending' ? 'pending' : this.formatBlockTimestamp(block) }</div> + <div>{ this.formatNumber(transaction.blockNumber) }</div> + </td> + <td className={ styles.txhash }> + { this.renderAddressName(transaction.from) } + </td> + <td className={ styles.txhash }> + <div className={ styles.eventType }> + { event.type }({ keys }) + </div> + <a href={ url } target='_blank'>{ this.formatHash(event.transactionHash) }</a> + </td> + <td className={ styles.eventDetails }> + <div className={ styles.eventParams }> + { values } + </div> + </td> + </tr> + ); + } + + formatHash (hash) { + if (!hash || hash.length <= 16) { + return hash; + } + + return `${hash.substr(2, 6)}...${hash.slice(-6)}`; + } + + renderAddressName (address, withName = true) { + return ( + <span className={ styles.eventAddress }> + <IdentityIcon center inline address={ address } className={ styles.eventIdentityicon } /> + { withName ? <IdentityName address={ address } /> : address } + </span> + ); + } + + renderParam (name, param) { + const { api } = this.context; + + switch (param.type) { + case 'address': + return ( + <InputAddress + disabled + text + className={ styles.input } + value={ param.value } + label={ name } /> + ); + + default: + let value; + if (api.util.isInstanceOf(param.value, BigNumber)) { + value = param.value.toFormat(0); + } else if (api.util.isArray(param.value)) { + value = api.util.bytesToHex(param.value); + } else { + value = param.value.toString(); + } + + return ( + <Input + disabled + className={ styles.input } + value={ value } + label={ name } /> + ); + } + } + + formatBlockTimestamp (block) { + if (!block) { + return null; + } + + return moment(block.timestamp).fromNow(); + } + + formatNumber (number) { + if (!number) { + return null; + } + + return new BigNumber(number).toFormat(); + } + + retrieveTransaction () { + const { event, fetchBlock, fetchTransaction } = this.props; + + fetchBlock(event.blockNumber); + fetchTransaction(event.transactionHash); + } +} + +function mapStateToProps (state) { + const { isTest } = state.nodeStatus; + const { blocks, transactions } = state.blockchain; + + return { + isTest, + blocks, + transactions + }; +} + +function mapDispatchToProps (dispatch) { + return bindActionCreators({ + fetchBlock, fetchTransaction + }, dispatch); +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Event); diff --git a/js/src/views/Contract/Events/Event/index.js b/js/src/views/Contract/Events/Event/index.js new file mode 100644 index 000000000..0925882d3 --- /dev/null +++ b/js/src/views/Contract/Events/Event/index.js @@ -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 './event'; diff --git a/js/src/views/Contract/Events/events.js b/js/src/views/Contract/Events/events.js index 7ff0341b3..0428a0ee3 100644 --- a/js/src/views/Contract/Events/events.js +++ b/js/src/views/Contract/Events/events.js @@ -14,11 +14,11 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. -import BigNumber from 'bignumber.js'; import React, { Component, PropTypes } from 'react'; import { Container, ContainerTitle } from '../../../ui'; +import Event from './Event'; import styles from '../contract.css'; export default class Events extends Component { @@ -27,106 +27,23 @@ export default class Events extends Component { } static propTypes = { - events: PropTypes.array, - isTest: PropTypes.bool - } - - state = { - transactions: {} - } - - componentDidMount () { - this.componentWillReceiveProps(this.props); - } - - componentWillReceiveProps (newProps) { - this.retrieveTransactions(newProps.events); + events: PropTypes.array } render () { - const { events, isTest } = this.props; - const { transactions } = this.state; + const { events } = this.props; if (!events || !events.length) { return null; } - const rows = events.map((event) => { - const transaction = transactions[event.transactionHash] || {}; - const classes = `${styles.event} ${styles[event.state]}`; - const url = `https://${isTest ? 'testnet.' : ''}etherscan.io/tx/${event.transactionHash}`; - const keys = Object.keys(event.params).map((key, index) => { - return <div className={ styles.key } key={ `${event.key}_key_${index}` }>{ key }</div>; - }); - const values = Object.values(event.params).map((value, index) => { - return ( - <div className={ styles.value } key={ `${event.key}_val_${index}` }> - { this.renderValue(value) } - </div> - ); - }); - - return ( - <tr className={ classes } key={ event.key }> - <td>{ event.state === 'pending' ? 'pending' : event.blockNumber.toFormat(0) }</td> - <td className={ styles.txhash }> - <div>{ transaction.from }</div> - <a href={ url } target='_blank'>{ event.transactionHash }</a> - </td> - <td> - <div>{ event.type } =></div> - { keys } - </td> - <td> - <div> </div> - { values } - </td> - </tr> - ); - }); - return ( <Container> <ContainerTitle title='events' /> <table className={ styles.events }> - <tbody>{ rows }</tbody> + <tbody>{ events.map((event) => <Event event={ event } key={ event.key } />) }</tbody> </table> </Container> ); } - - renderValue (value) { - const { api } = this.context; - - if (api.util.isInstanceOf(value, BigNumber)) { - return value.toFormat(0); - } else if (api.util.isArray(value)) { - return api.util.bytesToHex(value); - } - - return value.toString(); - } - - retrieveTransactions (events) { - const { api } = this.context; - const { transactions } = this.state; - const hashes = {}; - - events.forEach((event) => { - if (!hashes[event.transactionHash] && !transactions[event.transactionHash]) { - hashes[event.transactionHash] = true; - } - }); - - Promise - .all(Object.keys(hashes).map((hash) => api.eth.getTransactionByHash(hash))) - .then((newTransactions) => { - this.setState({ - transactions: newTransactions.reduce((store, transaction) => { - transactions[transaction.hash] = transaction; - return transactions; - }, transactions) - }); - }); - } } diff --git a/js/src/views/Contract/contract.css b/js/src/views/Contract/contract.css index 6956b8c50..4c6e93a3a 100644 --- a/js/src/views/Contract/contract.css +++ b/js/src/views/Contract/contract.css @@ -24,9 +24,12 @@ border-spacing: 0; } -.event { +.events tr { + line-height: 32px; vertical-align: top; - line-height: 26px; +} + +.event { } .event td { @@ -43,9 +46,6 @@ color: #aaa; } -.value { -} - .event td div { white-space: nowrap; } @@ -56,3 +56,35 @@ .pending { opacity: 0.5; } + +.timestamp { + padding-top: 1.5em; + text-align: right; + line-height: 1.5em; + opacity: 0.5; + white-space: nowrap; +} + +.eventDetails { +} + +.eventType { +} + +.eventParams { + padding-left: 2em; +} + +.eventValue { + margin-top: -16px; +} + +.eventAddress { + display: inline-block; + position: relative; +} + +.eventIdentityicon { + margin-bottom: -10px; + margin-right: 0.5em; +} diff --git a/js/src/views/Contract/contract.js b/js/src/views/Contract/contract.js index ec12c3ac9..394f8b360 100644 --- a/js/src/views/Contract/contract.js +++ b/js/src/views/Contract/contract.js @@ -116,7 +116,6 @@ class Contract extends Component { contract={ contract } values={ queryValues } /> <Events - isTest={ isTest } events={ allEvents } /> </Page> </div> From 03e2aa61e237b371c6cdaa9e5916d63007e5c9d4 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Tue, 25 Oct 2016 17:33:46 +0200 Subject: [PATCH 46/77] [ci skip] js-precompiled 20161025-153329 --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index dce80bdce..77cf7a71c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1207,7 +1207,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#28ab89f9944d4ec8943868c4147db98d853d88e4" +source = "git+https://github.com/ethcore/js-precompiled.git#18cc1f1aba75b9a7556f0461fb9379dbd0a34f02" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] From e71c7522100be8a6e2d8a2b47bc6422243016907 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Tue, 25 Oct 2016 17:39:19 +0200 Subject: [PATCH 47/77] Output git fetch/push to log files (#2862) * add -q options for fetch & push * Store log in .gitignored location * update GITLOG command location * Remove test branch --- js/scripts/release.sh | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/js/scripts/release.sh b/js/scripts/release.sh index de4bdc374..bcd93f9bc 100755 --- a/js/scripts/release.sh +++ b/js/scripts/release.sh @@ -10,7 +10,9 @@ function setup_git_user { } # change into the build directory -pushd `dirname $0` +BASEDIR=`dirname $0` +GITLOG=./.git/gitcommand.log +pushd $BASEDIR cd ../.build # variables @@ -33,12 +35,12 @@ git init # add local files and send it up setup_git_user git remote add origin https://${GITHUB_JS_PRECOMPILED}:@github.com/ethcore/js-precompiled.git -git fetch origin +git fetch origin 2>$GITLOG git checkout -b $CI_BUILD_REF_NAME git add . git commit -m "$UTCDATE [compiled]" git merge origin/$CI_BUILD_REF_NAME -X ours --commit -m "$UTCDATE [release]" -git push origin $CI_BUILD_REF_NAME +git push origin $CI_BUILD_REF_NAME 2>$GITLOG # back to root popd @@ -49,10 +51,10 @@ cargo update -p parity-ui-precompiled # add to git and push setup_git_user git remote set-url origin https://${GITHUB_JS_PRECOMPILED}:@github.com/ethcore/parity.git -git fetch origin +git fetch origin 2>$GITLOG git add . || true git commit -m "[ci skip] js-precompiled $UTCDATE" || true -git push origin || true +git push origin $CI_BUILD_REF_NAME 2>$GITLOG || true # exit with exit code exit 0 From 2d2e9c4d6e475a03706477556e01fa35ee42115e Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac <ngotchac@gmail.com> Date: Tue, 25 Oct 2016 17:54:01 +0200 Subject: [PATCH 48/77] Add Check and Change Password for an Account (#2861) * Added new RPC endpoints to JSAPI (#2389) * Added modal in Account Page to test & modify password (#2389) * Modify hint with password change // Better tabs (#2556) --- js/src/api/rpc/personal/personal.js | 10 + js/src/modals/PasswordManager/index.js | 17 + .../PasswordManager/passwordManager.css | 73 ++++ .../modals/PasswordManager/passwordManager.js | 383 ++++++++++++++++++ js/src/modals/index.js | 4 +- js/src/ui/Form/Input/input.js | 12 +- js/src/ui/Form/form.js | 12 +- js/src/ui/IdentityName/identityName.js | 7 +- js/src/views/Account/account.js | 42 +- 9 files changed, 550 insertions(+), 10 deletions(-) create mode 100644 js/src/modals/PasswordManager/index.js create mode 100644 js/src/modals/PasswordManager/passwordManager.css create mode 100644 js/src/modals/PasswordManager/passwordManager.js diff --git a/js/src/api/rpc/personal/personal.js b/js/src/api/rpc/personal/personal.js index 2609bc509..e35333102 100644 --- a/js/src/api/rpc/personal/personal.js +++ b/js/src/api/rpc/personal/personal.js @@ -33,6 +33,11 @@ export default class Personal { .execute('personal_confirmRequest', inNumber16(requestId), options, password); } + changePassword (account, password, newPassword) { + return this._transport + .execute('personal_changePassword', inAddress(account), password, newPassword); + } + generateAuthorizationToken () { return this._transport .execute('personal_generateAuthorizationToken'); @@ -105,6 +110,11 @@ export default class Personal { .execute('personal_signerEnabled'); } + testPassword (account, password) { + return this._transport + .execute('personal_testPassword', inAddress(account), password); + } + unlockAccount (account, password, duration = 1) { return this._transport .execute('personal_unlockAccount', inAddress(account), password, inNumber10(duration)); diff --git a/js/src/modals/PasswordManager/index.js b/js/src/modals/PasswordManager/index.js new file mode 100644 index 000000000..9676de163 --- /dev/null +++ b/js/src/modals/PasswordManager/index.js @@ -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 './passwordManager.js'; diff --git a/js/src/modals/PasswordManager/passwordManager.css b/js/src/modals/PasswordManager/passwordManager.css new file mode 100644 index 000000000..aa0f72cb3 --- /dev/null +++ b/js/src/modals/PasswordManager/passwordManager.css @@ -0,0 +1,73 @@ +/* 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/>. +*/ + +.accountContainer { + display: flex; + flex-direction: row; + margin-bottom: 1.5rem; +} + +.accountInfos { + display: flex; + flex-direction: column; + justify-content: space-around; +} + +.accountInfos > * { + margin: 0.25rem 0; +} + +.hintLabel { + text-transform: uppercase; + font-size: 0.7rem; + margin-right: 0.5rem; +} + +.accountAddress { + font-family: monospace; + font-size: 0.9rem; +} + +.accountName { + font-size: 1.1rem; +} + +.passwordHint { + font-size: 0.9rem; + color: lightgrey; +} + +.message { + margin-top: 1rem; + width: 100%; + height: 2.5rem; + text-align: center; + line-height: 2.5rem; + transition: height 350ms 0 !important; + overflow: hidden; +} + +.hideMessage { + height: 0; + background-color: transparent !important; +} + +.form { + margin-top: 0; + padding: 0 0.5rem 1rem; + background-color: rgba(255, 255, 255, 0.05); +} diff --git a/js/src/modals/PasswordManager/passwordManager.js b/js/src/modals/PasswordManager/passwordManager.js new file mode 100644 index 000000000..800fba929 --- /dev/null +++ b/js/src/modals/PasswordManager/passwordManager.js @@ -0,0 +1,383 @@ +// 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 ContentClear from 'material-ui/svg-icons/content/clear'; +import CheckIcon from 'material-ui/svg-icons/navigation/check'; +import SendIcon from 'material-ui/svg-icons/content/send'; + +import { Tabs, Tab } from 'material-ui/Tabs'; +import Paper from 'material-ui/Paper'; + +import Form, { Input } from '../../ui/Form'; +import { Button, Modal, IdentityName, IdentityIcon } from '../../ui'; + +import styles from './passwordManager.css'; + +const TEST_ACTION = 'TEST_ACTION'; +const CHANGE_ACTION = 'CHANGE_ACTION'; + +export default class PasswordManager extends Component { + static contextTypes = { + api: PropTypes.object.isRequired + } + + static propTypes = { + account: PropTypes.object.isRequired, + onClose: PropTypes.func + } + + state = { + action: TEST_ACTION, + waiting: false, + showMessage: false, + message: { value: '', success: true }, + currentPass: '', + newPass: '', + repeatNewPass: '', + repeatValid: true, + passwordHint: this.props.account.meta && this.props.account.meta.passwordHint || '' + } + + render () { + return ( + <Modal + actions={ this.renderDialogActions() } + title='Password Manager' + visible> + { this.renderAccount() } + { this.renderPage() } + { this.renderMessage() } + </Modal> + ); + } + + renderMessage () { + const { message, showMessage } = this.state; + + const style = message.success + ? { + backgroundColor: 'rgba(174, 213, 129, 0.75)' + } + : { + backgroundColor: 'rgba(229, 115, 115, 0.75)' + }; + + const classes = [ styles.message ]; + + if (!showMessage) { + classes.push(styles.hideMessage); + } + + return ( + <Paper + zDepth={ 1 } + style={ style } + className={ classes.join(' ') }> + { message.value } + </Paper> + ); + } + + renderAccount () { + const { account } = this.props; + const { address, meta } = account; + + const passwordHint = meta && meta.passwordHint + ? ( + <span className={ styles.passwordHint }> + <span className={ styles.hintLabel }>Hint </span> + { meta.passwordHint } + </span> + ) + : null; + + return ( + <div className={ styles.accountContainer }> + <IdentityIcon + address={ address } + /> + <div className={ styles.accountInfos }> + <IdentityName + className={ styles.accountName } + address={ address } + unknown + /> + <span className={ styles.accountAddress }> + { address } + </span> + { passwordHint } + </div> + </div> + ); + } + + renderPage () { + const { account } = this.props; + const { waiting, repeatValid } = this.state; + const disabled = !!waiting; + + const repeatError = repeatValid + ? null + : 'the two passwords differ'; + + const { meta } = account; + const passwordHint = meta && meta.passwordHint || ''; + + return ( + <Tabs + inkBarStyle={ { + backgroundColor: 'rgba(255, 255, 255, 0.55)' + } } + tabItemContainerStyle={ { + backgroundColor: 'rgba(255, 255, 255, 0.05)' + } } + > + <Tab + onActive={ this.handleTestActive } + label='Test Password' + > + <Form + className={ styles.form } + > + <div> + <Input + label='password' + hint='your current password for this account' + type='password' + submitOnBlur={ false } + disabled={ disabled } + onSubmit={ this.handleTestPassword } + onChange={ this.onEditCurrent } /> + </div> + </Form> + </Tab> + <Tab + onActive={ this.handleChangeActive } + label='Change Password' + > + <Form + className={ styles.form } + > + <div> + <Input + label='current password' + hint='your current password for this account' + type='password' + submitOnBlur={ false } + disabled={ disabled } + onSubmit={ this.handleChangePassword } + onChange={ this.onEditCurrent } /> + + <Input + label='new password' + hint='the new password for this account' + type='password' + submitOnBlur={ false } + disabled={ disabled } + onSubmit={ this.handleChangePassword } + onChange={ this.onEditNew } /> + <Input + label='repeat new password' + hint='repeat the new password for this account' + type='password' + submitOnBlur={ false } + error={ repeatError } + disabled={ disabled } + onSubmit={ this.handleChangePassword } + onChange={ this.onEditRepeatNew } /> + + <Input + label='new password hint' + hint='hint for the new password' + submitOnBlur={ false } + value={ passwordHint } + disabled={ disabled } + onSubmit={ this.handleChangePassword } + onChange={ this.onEditHint } /> + </div> + </Form> + </Tab> + </Tabs> + ); + } + + renderDialogActions () { + const { onClose } = this.props; + const { action, waiting, repeatValid } = this.state; + + const cancelBtn = ( + <Button + icon={ <ContentClear /> } + label='Cancel' + onClick={ onClose } /> + ); + + if (waiting) { + const waitingBtn = ( + <Button + disabled + label='Wait...' /> + ); + + return [ cancelBtn, waitingBtn ]; + } + + if (action === TEST_ACTION) { + const testBtn = ( + <Button + icon={ <CheckIcon /> } + label='Test' + onClick={ this.handleTestPassword } /> + ); + + return [ cancelBtn, testBtn ]; + } + + const changeBtn = ( + <Button + disabled={ !repeatValid } + icon={ <SendIcon /> } + label='Change' + onClick={ this.handleChangePassword } /> + ); + + return [ cancelBtn, changeBtn ]; + } + + onEditCurrent = (event, value) => { + this.setState({ + currentPass: value, + showMessage: false + }); + } + + onEditNew = (event, value) => { + const repeatValid = value === this.state.repeatNewPass; + + this.setState({ + newPass: value, + showMessage: false, + repeatValid + }); + } + + onEditRepeatNew = (event, value) => { + const repeatValid = value === this.state.newPass; + + this.setState({ + repeatNewPass: value, + showMessage: false, + repeatValid + }); + } + + onEditHint = (event, value) => { + this.setState({ + passwordHint: value, + showMessage: false + }); + } + + handleTestActive = () => { + this.setState({ + action: TEST_ACTION, + showMessage: false + }); + } + + handleChangeActive = () => { + this.setState({ + action: CHANGE_ACTION, + showMessage: false + }); + } + + handleTestPassword = () => { + const { account } = this.props; + const { currentPass } = this.state; + + this.setState({ waiting: true, showMessage: false }); + + this.context + .api.personal + .testPassword(account.address, currentPass) + .then(correct => { + const message = correct + ? { value: 'This password is correct', success: true } + : { value: 'This password is not correct', success: false }; + + this.setState({ waiting: false, message, showMessage: true }); + }) + .catch(e => { + console.error('passwordManager::handleTestPassword', e); + this.setState({ waiting: false }); + }); + } + + handleChangePassword = () => { + const { account } = this.props; + const { currentPass, newPass, repeatNewPass, passwordHint } = this.state; + + if (repeatNewPass !== newPass) { + return; + } + + this.setState({ waiting: true, showMessage: false }); + + this.context + .api.personal + .testPassword(account.address, currentPass) + .then(correct => { + if (!correct) { + const message = { + value: 'This provided current password is not correct', + success: false + }; + + this.setState({ waiting: false, message, showMessage: true }); + + return false; + } + + const meta = Object.assign({}, account.meta, { + passwordHint + }); + + return Promise.all([ + this.context + .api.personal + .setAccountMeta(account.address, meta), + + this.context + .api.personal + .changePassword(account.address, currentPass, newPass) + ]) + .then(() => { + const message = { + value: 'Your password has been successfully changed', + success: true + }; + + this.setState({ waiting: false, message, showMessage: true }); + }); + }) + .catch(e => { + console.error('passwordManager::handleChangePassword', e); + this.setState({ waiting: false }); + }); + } +} diff --git a/js/src/modals/index.js b/js/src/modals/index.js index d5eb91971..0e8c33563 100644 --- a/js/src/modals/index.js +++ b/js/src/modals/index.js @@ -23,6 +23,7 @@ import ExecuteContract from './ExecuteContract'; import FirstRun from './FirstRun'; import Shapeshift from './Shapeshift'; import Transfer from './Transfer'; +import PasswordManager from './PasswordManager'; export { AddAddress, @@ -33,5 +34,6 @@ export { ExecuteContract, FirstRun, Shapeshift, - Transfer + Transfer, + PasswordManager }; diff --git a/js/src/ui/Form/Input/input.js b/js/src/ui/Form/Input/input.js index d21637391..0c9206d0e 100644 --- a/js/src/ui/Form/Input/input.js +++ b/js/src/ui/Form/Input/input.js @@ -44,13 +44,18 @@ export default class Input extends Component { onSubmit: PropTypes.func, rows: PropTypes.number, type: PropTypes.string, + submitOnBlur: PropTypes.bool, value: PropTypes.oneOfType([ PropTypes.number, PropTypes.string ]) } + static defaultProps = { + submitOnBlur: true + } + state = { - value: this.props.value + value: this.props.value || '' } componentWillReceiveProps (newProps) { @@ -97,8 +102,11 @@ export default class Input extends Component { onBlur = (event) => { const { value } = event.target; + const { submitOnBlur } = this.props; - this.onSubmit(value); + if (submitOnBlur) { + this.onSubmit(value); + } this.props.onBlur && this.props.onBlur(event); } diff --git a/js/src/ui/Form/form.js b/js/src/ui/Form/form.js index 930f3c51d..c3cf2588b 100644 --- a/js/src/ui/Form/form.js +++ b/js/src/ui/Form/form.js @@ -20,15 +20,23 @@ import styles from './form.css'; export default class Form extends Component { static propTypes = { - children: PropTypes.node + children: PropTypes.node, + className: PropTypes.string } render () { + const { className } = this.props; + const classes = [ styles.form ]; + + if (className) { + classes.push(className); + } + // HACK: hidden inputs to disable Chrome's autocomplete return ( <form autoComplete='new-password' - className={ styles.form }> + className={ classes.join(' ') }> <div className={ styles.autofill }> <input type='text' name='fakeusernameremembered' /> <input type='password' name='fakepasswordremembered' /> diff --git a/js/src/ui/IdentityName/identityName.js b/js/src/ui/IdentityName/identityName.js index 11e7c7d96..6b6c75be2 100644 --- a/js/src/ui/IdentityName/identityName.js +++ b/js/src/ui/IdentityName/identityName.js @@ -22,6 +22,7 @@ const defaultName = 'UNNAMED'; class IdentityName extends Component { static propTypes = { + className: PropTypes.string, address: PropTypes.string, accountsInfo: PropTypes.object, tokens: PropTypes.object, @@ -31,7 +32,7 @@ class IdentityName extends Component { } render () { - const { address, accountsInfo, tokens, empty, shorten, unknown } = this.props; + const { address, accountsInfo, tokens, empty, shorten, unknown, className } = this.props; const account = accountsInfo[address] || tokens[address]; const hasAccount = account && (!account.meta || !account.meta.deleted); @@ -47,7 +48,9 @@ class IdentityName extends Component { : fallback; return ( - <span>{ name && name.length ? name : fallback }</span> + <span className={ className }> + { name && name.length ? name : fallback } + </span> ); } diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js index 10fcd9f3c..ecdf1cfd8 100644 --- a/js/src/views/Account/account.js +++ b/js/src/views/Account/account.js @@ -19,8 +19,9 @@ 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 LockIcon from 'material-ui/svg-icons/action/lock'; -import { EditMeta, Shapeshift, Transfer } from '../../modals'; +import { EditMeta, Shapeshift, Transfer, PasswordManager } from '../../modals'; import { Actionbar, Button, Page } from '../../ui'; import shapeshiftBtn from '../../../assets/images/shapeshift-btn.png'; @@ -44,7 +45,8 @@ class Account extends Component { state = { showEditDialog: false, showFundDialog: false, - showTransferDialog: false + showTransferDialog: false, + showPasswordDialog: false } render () { @@ -63,6 +65,7 @@ class Account extends Component { { this.renderEditDialog(account) } { this.renderFundDialog() } { this.renderTransferDialog() } + { this.renderPasswordDialog() } { this.renderActionbar() } <Page> <Header @@ -93,7 +96,12 @@ class Account extends Component { key='editmeta' icon={ <ContentCreate /> } label='edit' - onClick={ this.onEditClick } /> + onClick={ this.onEditClick } />, + <Button + key='passwordManager' + icon={ <LockIcon /> } + label='password' + onClick={ this.onPasswordClick } /> ]; return ( @@ -156,6 +164,24 @@ class Account extends Component { ); } + renderPasswordDialog () { + const { showPasswordDialog } = this.state; + + if (!showPasswordDialog) { + return null; + } + + const { address } = this.props.params; + const { accounts } = this.props; + const account = accounts[address]; + + return ( + <PasswordManager + account={ account } + onClose={ this.onPasswordClose } /> + ); + } + onEditClick = () => { this.setState({ showEditDialog: !this.state.showEditDialog @@ -181,6 +207,16 @@ class Account extends Component { onTransferClose = () => { this.onTransferClick(); } + + onPasswordClick = () => { + this.setState({ + showPasswordDialog: !this.state.showPasswordDialog + }); + } + + onPasswordClose = () => { + this.onPasswordClick(); + } } function mapStateToProps (state) { From 135d5d0e4c30456d5d3a991b0dc7a9fdbbe766bb Mon Sep 17 00:00:00 2001 From: Arkadiy Paronyan <arkady.paronyan@gmail.com> Date: Tue, 25 Oct 2016 18:40:01 +0200 Subject: [PATCH 49/77] Snapshot fixes and optimizations (#2863) --- ethcore/src/snapshot/mod.rs | 14 ++--- ethcore/src/snapshot/service.rs | 80 +++++++++++++++------------ sync/src/chain.rs | 14 +++-- sync/src/sync_io.rs | 6 ++ sync/src/tests/helpers.rs | 4 ++ util/network/src/host.rs | 10 +++- util/network/src/session.rs | 4 +- util/src/journaldb/overlayrecentdb.rs | 4 +- 8 files changed, 84 insertions(+), 52 deletions(-) diff --git a/ethcore/src/snapshot/mod.rs b/ethcore/src/snapshot/mod.rs index 4523f4f16..8b032c0e1 100644 --- a/ethcore/src/snapshot/mod.rs +++ b/ethcore/src/snapshot/mod.rs @@ -38,6 +38,7 @@ use util::kvdb::Database; use util::trie::{TrieDB, TrieDBMut, Trie, TrieMut}; use util::sha3::SHA3_NULL_RLP; use rlp::{RlpStream, Stream, UntrustedRlp, View}; +use bloom_journal::Bloom; use self::account::Account; use self::block::AbridgedBlock; @@ -390,6 +391,7 @@ pub struct StateRebuilder { state_root: H256, code_map: HashMap<H256, Bytes>, // maps code hashes to code itself. missing_code: HashMap<H256, Vec<H256>>, // maps code hashes to lists of accounts missing that code. + bloom: Bloom, } impl StateRebuilder { @@ -400,6 +402,7 @@ impl StateRebuilder { state_root: SHA3_NULL_RLP, code_map: HashMap::new(), missing_code: HashMap::new(), + bloom: StateDB::load_bloom(&*db), } } @@ -462,9 +465,6 @@ impl StateRebuilder { let backing = self.db.backing().clone(); - // bloom has to be updated - let mut bloom = StateDB::load_bloom(&backing); - // batch trie writes { let mut account_trie = if self.state_root != SHA3_NULL_RLP { @@ -475,17 +475,17 @@ impl StateRebuilder { for (hash, thin_rlp) in pairs { if &thin_rlp[..] != &empty_rlp[..] { - bloom.set(&*hash); + self.bloom.set(&*hash); } try!(account_trie.insert(&hash, &thin_rlp)); } } - let bloom_journal = bloom.drain_journal(); + let bloom_journal = self.bloom.drain_journal(); let mut batch = backing.transaction(); try!(StateDB::commit_bloom(&mut batch, bloom_journal)); try!(self.db.inject(&mut batch)); - try!(backing.write(batch).map_err(::util::UtilError::SimpleString)); + backing.write_buffered(batch); trace!(target: "snapshot", "current state root: {:?}", self.state_root); Ok(()) } @@ -628,7 +628,7 @@ impl BlockRebuilder { } else { self.chain.insert_unordered_block(&mut batch, &block_bytes, receipts, None, is_best, false); } - self.db.write(batch).expect("Error writing to the DB"); + self.db.write_buffered(batch); self.chain.commit(); parent_hash = BlockView::new(&block_bytes).hash(); diff --git a/ethcore/src/snapshot/service.rs b/ethcore/src/snapshot/service.rs index 57782e6cd..b3aaa017e 100644 --- a/ethcore/src/snapshot/service.rs +++ b/ethcore/src/snapshot/service.rs @@ -74,6 +74,7 @@ struct Restoration { snappy_buffer: Bytes, final_state_root: H256, guard: Guard, + db: Arc<Database>, } struct RestorationParams<'a> { @@ -105,12 +106,13 @@ impl Restoration { manifest: manifest, state_chunks_left: state_chunks, block_chunks_left: block_chunks, - state: StateRebuilder::new(raw_db, params.pruning), + state: StateRebuilder::new(raw_db.clone(), params.pruning), blocks: blocks, writer: params.writer, snappy_buffer: Vec::new(), final_state_root: root, guard: params.guard, + db: raw_db, }) } @@ -467,39 +469,46 @@ impl Service { /// Feed a chunk of either kind. no-op if no restoration or status is wrong. fn feed_chunk(&self, hash: H256, chunk: &[u8], is_state: bool) -> Result<(), Error> { // TODO: be able to process block chunks and state chunks at same time? - let mut restoration = self.restoration.lock(); + let (result, db) = { + let mut restoration = self.restoration.lock(); - match self.status() { - RestorationStatus::Inactive | RestorationStatus::Failed => Ok(()), - RestorationStatus::Ongoing { .. } => { - let res = { - let rest = match *restoration { - Some(ref mut r) => r, - None => return Ok(()), - }; - - match is_state { - true => rest.feed_state(hash, chunk), - false => rest.feed_blocks(hash, chunk, &*self.engine), - }.map(|_| rest.is_done()) - }; - - match res { - Ok(is_done) => { - match is_state { - true => self.state_chunks.fetch_add(1, Ordering::SeqCst), - false => self.block_chunks.fetch_add(1, Ordering::SeqCst), + match self.status() { + RestorationStatus::Inactive | RestorationStatus::Failed => return Ok(()), + RestorationStatus::Ongoing { .. } => { + let (res, db) = { + let rest = match *restoration { + Some(ref mut r) => r, + None => return Ok(()), }; - match is_done { - true => self.finalize_restoration(&mut *restoration), - false => Ok(()) + (match is_state { + true => rest.feed_state(hash, chunk), + false => rest.feed_blocks(hash, chunk, &*self.engine), + }.map(|_| rest.is_done()), rest.db.clone()) + }; + + let res = match res { + Ok(is_done) => { + match is_state { + true => self.state_chunks.fetch_add(1, Ordering::SeqCst), + false => self.block_chunks.fetch_add(1, Ordering::SeqCst), + }; + + match is_done { + true => { + try!(db.flush().map_err(::util::UtilError::SimpleString)); + self.finalize_restoration(&mut *restoration) + }, + false => Ok(()) + } } - } - other => other.map(drop), + other => other.map(drop), + }; + (res, db) } } - } + }; + result.and_then(|_| db.flush().map_err(|e| ::util::UtilError::SimpleString(e).into())) } /// Feed a state chunk to be processed synchronously. @@ -549,8 +558,9 @@ impl SnapshotService for Service { } fn begin_restore(&self, manifest: ManifestData) { - self.io_channel.send(ClientIoMessage::BeginRestoration(manifest)) - .expect("snapshot service and io service are kept alive by client service; qed"); + if let Err(e) = self.io_channel.send(ClientIoMessage::BeginRestoration(manifest)) { + trace!("Error sending snapshot service message: {:?}", e); + } } fn abort_restore(&self) { @@ -559,13 +569,15 @@ impl SnapshotService for Service { } fn restore_state_chunk(&self, hash: H256, chunk: Bytes) { - self.io_channel.send(ClientIoMessage::FeedStateChunk(hash, chunk)) - .expect("snapshot service and io service are kept alive by client service; qed"); + if let Err(e) = self.io_channel.send(ClientIoMessage::FeedStateChunk(hash, chunk)) { + trace!("Error sending snapshot service message: {:?}", e); + } } fn restore_block_chunk(&self, hash: H256, chunk: Bytes) { - self.io_channel.send(ClientIoMessage::FeedBlockChunk(hash, chunk)) - .expect("snapshot service and io service are kept alive by client service; qed"); + if let Err(e) = self.io_channel.send(ClientIoMessage::FeedBlockChunk(hash, chunk)) { + trace!("Error sending snapshot service message: {:?}", e); + } } } diff --git a/sync/src/chain.rs b/sync/src/chain.rs index 850dff228..916e7424e 100644 --- a/sync/src/chain.rs +++ b/sync/src/chain.rs @@ -1253,7 +1253,12 @@ impl ChainSync { } peer.asking = asking; peer.ask_time = time::precise_time_s(); - if let Err(e) = sync.send(peer_id, packet_id, packet) { + let result = if packet_id >= ETH_PACKET_COUNT { + sync.send_protocol(WARP_SYNC_PROTOCOL_ID, peer_id, packet_id, packet) + } else { + sync.send(peer_id, packet_id, packet) + }; + if let Err(e) = result { debug!(target:"sync", "Error sending request: {:?}", e); sync.disable_peer(peer_id); } @@ -1270,8 +1275,9 @@ impl ChainSync { /// Called when peer sends us new transactions fn on_peer_transactions(&mut self, io: &mut SyncIo, peer_id: PeerId, r: &UntrustedRlp) -> Result<(), PacketDecodeError> { - // accepting transactions once only fully synced - if !io.is_chain_queue_empty() { + // Accept transactions only when fully synced + if !io.is_chain_queue_empty() || self.state != SyncState::Idle || self.state != SyncState::NewBlocks { + trace!(target: "sync", "{} Ignoring transactions while syncing", peer_id); return Ok(()); } if !self.peers.get(&peer_id).map_or(false, |p| p.can_sync()) { @@ -1570,7 +1576,7 @@ impl ChainSync { SNAPSHOT_MANIFEST_PACKET => self.on_snapshot_manifest(io, peer, &rlp), SNAPSHOT_DATA_PACKET => self.on_snapshot_data(io, peer, &rlp), _ => { - debug!(target: "sync", "Unknown packet {}", packet_id); + debug!(target: "sync", "{}: Unknown packet {}", peer, packet_id); Ok(()) } }; diff --git a/sync/src/sync_io.rs b/sync/src/sync_io.rs index 25d235c60..c78074aed 100644 --- a/sync/src/sync_io.rs +++ b/sync/src/sync_io.rs @@ -34,6 +34,8 @@ pub trait SyncIo { fn respond(&mut self, packet_id: PacketId, data: Vec<u8>) -> Result<(), NetworkError>; /// Send a packet to a peer. fn send(&mut self, peer_id: PeerId, packet_id: PacketId, data: Vec<u8>) -> Result<(), NetworkError>; + /// Send a packet to a peer using specified protocol. + fn send_protocol(&mut self, protocol: ProtocolId, peer_id: PeerId, packet_id: PacketId, data: Vec<u8>) -> Result<(), NetworkError>; /// Get the blockchain fn chain(&self) -> &BlockChainClient; /// Get the snapshot service. @@ -98,6 +100,10 @@ impl<'s, 'h> SyncIo for NetSyncIo<'s, 'h> { self.network.send(peer_id, packet_id, data) } + fn send_protocol(&mut self, protocol: ProtocolId, peer_id: PeerId, packet_id: PacketId, data: Vec<u8>) -> Result<(), NetworkError>{ + self.network.send_protocol(protocol, peer_id, packet_id, data) + } + fn chain(&self) -> &BlockChainClient { self.chain } diff --git a/sync/src/tests/helpers.rs b/sync/src/tests/helpers.rs index 801db234d..202ab4f17 100644 --- a/sync/src/tests/helpers.rs +++ b/sync/src/tests/helpers.rs @@ -78,6 +78,10 @@ impl<'p> SyncIo for TestIo<'p> { Ok(()) } + fn send_protocol(&mut self, _protocol: ProtocolId, peer_id: PeerId, packet_id: PacketId, data: Vec<u8>) -> Result<(), NetworkError> { + self.send(peer_id, packet_id, data) + } + fn chain(&self) -> &BlockChainClient { self.chain } diff --git a/util/network/src/host.rs b/util/network/src/host.rs index 866534397..177a44843 100644 --- a/util/network/src/host.rs +++ b/util/network/src/host.rs @@ -241,9 +241,14 @@ impl<'s> NetworkContext<'s> { /// Send a packet over the network to another peer. pub fn send(&self, peer: PeerId, packet_id: PacketId, data: Vec<u8>) -> Result<(), NetworkError> { + self.send_protocol(self.protocol, peer, packet_id, data) + } + + /// Send a packet over the network to another peer using specified protocol. + pub fn send_protocol(&self, protocol: ProtocolId, peer: PeerId, packet_id: PacketId, data: Vec<u8>) -> Result<(), NetworkError> { let session = self.resolve_session(peer); if let Some(session) = session { - try!(session.lock().send_packet(self.io, self.protocol, packet_id as u8, &data)); + try!(session.lock().send_packet(self.io, protocol, packet_id as u8, &data)); } else { trace!(target: "network", "Send: Peer no longer exist") } @@ -911,7 +916,7 @@ impl Host { } } - fn update_nodes(&self, io: &IoContext<NetworkIoMessage>, node_changes: TableUpdates) { + fn update_nodes(&self, _io: &IoContext<NetworkIoMessage>, node_changes: TableUpdates) { let mut to_remove: Vec<PeerId> = Vec::new(); { let sessions = self.sessions.write(); @@ -926,7 +931,6 @@ impl Host { } for i in to_remove { trace!(target: "network", "Removed from node table: {}", i); - self.kill_connection(i, io, false); } self.nodes.write().update(node_changes, &*self.reserved_nodes.read()); } diff --git a/util/network/src/session.rs b/util/network/src/session.rs index 1791a441d..8d5578e83 100644 --- a/util/network/src/session.rs +++ b/util/network/src/session.rs @@ -395,7 +395,7 @@ impl Session { PACKET_PEERS => Ok(SessionData::None), PACKET_USER ... PACKET_LAST => { let mut i = 0usize; - while packet_id < self.info.capabilities[i].id_offset { + while packet_id > self.info.capabilities[i].id_offset + self.info.capabilities[i].packet_count { i += 1; if i == self.info.capabilities.len() { debug!(target: "network", "Unknown packet: {:?}", packet_id); @@ -469,7 +469,7 @@ impl Session { offset += caps[i].packet_count; i += 1; } - trace!(target: "network", "Hello: {} v{} {} {:?}", client_version, protocol, id, caps); + debug!(target: "network", "Hello: {} v{} {} {:?}", client_version, protocol, id, caps); self.info.protocol_version = protocol; self.info.client_version = client_version; self.info.capabilities = caps; diff --git a/util/src/journaldb/overlayrecentdb.rs b/util/src/journaldb/overlayrecentdb.rs index bf01567fb..e58fe2522 100644 --- a/util/src/journaldb/overlayrecentdb.rs +++ b/util/src/journaldb/overlayrecentdb.rs @@ -348,13 +348,13 @@ impl JournalDB for OverlayRecentDB { match rc { 0 => {} 1 => { - if try!(self.backing.get(self.column, &key)).is_some() { + if cfg!(debug_assertions) && try!(self.backing.get(self.column, &key)).is_some() { return Err(BaseDataError::AlreadyExists(key).into()); } batch.put(self.column, &key, &value) } -1 => { - if try!(self.backing.get(self.column, &key)).is_none() { + if cfg!(debug_assertions) && try!(self.backing.get(self.column, &key)).is_none() { return Err(BaseDataError::NegativelyReferencedHash(key).into()); } batch.delete(self.column, &key) From f81787d6038d72358288e8e3b7f20f9d88d55944 Mon Sep 17 00:00:00 2001 From: NikVolf <nikvolf@gmail.com> Date: Tue, 25 Oct 2016 21:28:54 +0300 Subject: [PATCH 50/77] ipc version bump --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index dce80bdce..d2cb60515 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -812,7 +812,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "json-ipc-server" version = "0.2.4" -source = "git+https://github.com/ethcore/json-ipc-server.git#5fbd0253750d3097b9a8fb27effa84c18d630bbb" +source = "git+https://github.com/ethcore/json-ipc-server.git#4642cd03ec1d23db89df80d22d5a88e7364ab885" 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)", From d1d82e787b370a921ccac804eadcd4ec9693bac6 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Tue, 25 Oct 2016 22:27:13 +0200 Subject: [PATCH 51/77] Make GitLab js-precompiled really update Cargo.toml in main repo (#2869) * Src outputs into build.sh * reset detatched head * add .dist * testing for this branch * update comments, be explicit is what we are doing * [ci skip] js-precompiled 20161025-173946 * Revert "[ci skip] js-precompiled 20161025-173946" This reverts commit 0d23f7683c6e18e4642566313963c130684afa90. * remove testing branch * typo --- js/.gitignore | 1 + js/scripts/build.sh | 11 +++++++++-- js/scripts/release.sh | 30 +++++++++++------------------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/js/.gitignore b/js/.gitignore index f1c885637..b3ece001c 100644 --- a/js/.gitignore +++ b/js/.gitignore @@ -3,4 +3,5 @@ npm-debug.log build .build .coverage +.dist .happypack diff --git a/js/scripts/build.sh b/js/scripts/build.sh index 1f3f44ceb..a6f4a913c 100755 --- a/js/scripts/build.sh +++ b/js/scripts/build.sh @@ -6,8 +6,15 @@ cd .. # run build (production) and store the exit code EXITCODE=0 -rm -rf .build -npm run ci:build || EXITCODE=1 +BUILDDIR=./.dist +rm -rf $BUILDDIR +mkdir -p $BUILDDIR/src +BUILD_DEST=$BUILDDIR/build npm run ci:build || EXITCODE=1 + +# Copy rust files +cp Cargo.precompiled.toml $BUILDDIR/Cargo.toml +cp build.rs $BUILDDIR +cp src/lib.rs* $BUILDDIR/src # back to root popd diff --git a/js/scripts/release.sh b/js/scripts/release.sh index bcd93f9bc..392bdd3b8 100755 --- a/js/scripts/release.sh +++ b/js/scripts/release.sh @@ -13,21 +13,11 @@ function setup_git_user { BASEDIR=`dirname $0` GITLOG=./.git/gitcommand.log pushd $BASEDIR -cd ../.build +cd ../.dist # variables UTCDATE=`date -u "+%Y%m%d-%H%M%S"` -# Create proper directory structure -mkdir -p build -mv *.* build -mkdir -p src - -# Copy rust files -cp ../Cargo.precompiled.toml Cargo.toml -cp ../build.rs . -cp ../src/lib.rs* ./src/ - # init git rm -rf ./.git git init @@ -40,21 +30,23 @@ git checkout -b $CI_BUILD_REF_NAME git add . git commit -m "$UTCDATE [compiled]" git merge origin/$CI_BUILD_REF_NAME -X ours --commit -m "$UTCDATE [release]" -git push origin $CI_BUILD_REF_NAME 2>$GITLOG +git push origin HEAD:refs/heads/$CI_BUILD_REF_NAME 2>$GITLOG # back to root popd -# bump js-precompiled -cargo update -p parity-ui-precompiled - -# add to git and push +# inti git with right origin setup_git_user git remote set-url origin https://${GITHUB_JS_PRECOMPILED}:@github.com/ethcore/parity.git -git fetch origin 2>$GITLOG + +# at this point we have a detached head on GitLab, reset +git reset --hard origin/$CI_BUILD_REF_NAME 2>$GITLOG + +# bump js-precompiled, add, commit & push +cargo update -p parity-ui-precompiled git add . || true -git commit -m "[ci skip] js-precompiled $UTCDATE" || true -git push origin $CI_BUILD_REF_NAME 2>$GITLOG || true +git commit -m "[ci skip] js-precompiled $UTCDATE" +git push origin HEAD:refs/heads/$CI_BUILD_REF_NAME 2>$GITLOG # exit with exit code exit 0 From 33748c2046c3ede7e5b79ae9a77944775bd0b9ff Mon Sep 17 00:00:00 2001 From: Robert Habermeier <rphmeier@gmail.com> Date: Tue, 25 Oct 2016 22:34:52 +0200 Subject: [PATCH 52/77] Sweep some more panics (#2848) * purge unwraps from ethcrypto, ethstore * sweep panics from util --- Cargo.lock | 1 + ethcrypto/src/lib.rs | 8 ++++++-- ethstore/Cargo.toml | 1 + ethstore/src/dir/disk.rs | 12 +++++++----- ethstore/src/ethstore.rs | 14 +++++++------- ethstore/src/lib.rs | 7 +++++-- ethstore/src/presale.rs | 3 ++- util/src/common.rs | 4 ++-- util/src/journaldb/earlymergedb.rs | 5 ++++- util/src/kvdb.rs | 11 +++++++---- util/src/memorydb.rs | 2 +- util/src/trie/triedb.rs | 5 +++-- util/src/trie/triedbmut.rs | 3 ++- 13 files changed, 48 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 78eabd75e..d6ccbf86f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -613,6 +613,7 @@ dependencies = [ "itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.15 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/ethcrypto/src/lib.rs b/ethcrypto/src/lib.rs index 7a1aba48c..103e750e6 100644 --- a/ethcrypto/src/lib.rs +++ b/ethcrypto/src/lib.rs @@ -166,7 +166,9 @@ pub mod ecies { /// Encrypt a message with a public key pub fn encrypt(public: &Public, shared_mac: &[u8], plain: &[u8]) -> Result<Vec<u8>, Error> { - let r = Random.generate().unwrap(); + let r = Random.generate() + .expect("context known to have key-generation capabilities; qed"); + let z = try!(ecdh::agree(r.secret(), public)); let mut key = [0u8; 32]; let mut mkey = [0u8; 32]; @@ -201,7 +203,9 @@ pub mod ecies { /// Encrypt a message with a public key pub fn encrypt_single_message(public: &Public, plain: &[u8]) -> Result<Vec<u8>, Error> { - let r = Random.generate().unwrap(); + let r = Random.generate() + .expect("context known to have key-generation capabilities"); + let z = try!(ecdh::agree(r.secret(), public)); let mut key = [0u8; 32]; let mut mkey = [0u8; 32]; diff --git a/ethstore/Cargo.toml b/ethstore/Cargo.toml index 7fa2a6890..03347cbd7 100644 --- a/ethstore/Cargo.toml +++ b/ethstore/Cargo.toml @@ -18,6 +18,7 @@ docopt = { version = "0.6", optional = true } time = "0.1.34" lazy_static = "0.2" itertools = "0.4" +parking_lot = "0.3" ethcrypto = { path = "../ethcrypto" } [build-dependencies] diff --git a/ethstore/src/dir/disk.rs b/ethstore/src/dir/disk.rs index fe1c46e63..6616ec15d 100644 --- a/ethstore/src/dir/disk.rs +++ b/ethstore/src/dir/disk.rs @@ -28,7 +28,9 @@ const IGNORED_FILES: &'static [&'static str] = &["thumbs.db", "address_book.json fn restrict_permissions_to_owner(file_path: &Path) -> Result<(), i32> { use std::ffi; use libc; - let cstr = ffi::CString::new(file_path.to_str().unwrap()).unwrap(); + + let cstr = try!(ffi::CString::new(&*file_path.to_string_lossy()) + .map_err(|_| -1)); match unsafe { libc::chmod(cstr.as_ptr(), libc::S_IWUSR | libc::S_IRUSR) } { 0 => Ok(()), x => Err(x), @@ -63,15 +65,15 @@ impl DiskDirectory { let paths = try!(fs::read_dir(&self.path)) .flat_map(Result::ok) .filter(|entry| { - let metadata = entry.metadata(); + let metadata = entry.metadata().ok(); let file_name = entry.file_name(); - let name = file_name.to_str().unwrap(); + let name = file_name.to_string_lossy(); // filter directories - metadata.is_ok() && !metadata.unwrap().is_dir() && + metadata.map_or(false, |m| !m.is_dir()) && // hidden files !name.starts_with(".") && // other ignored files - !IGNORED_FILES.contains(&name) + !IGNORED_FILES.contains(&&*name) }) .map(|entry| entry.path()) .collect::<Vec<PathBuf>>(); diff --git a/ethstore/src/ethstore.rs b/ethstore/src/ethstore.rs index faaea8f9a..4360a39f0 100644 --- a/ethstore/src/ethstore.rs +++ b/ethstore/src/ethstore.rs @@ -15,7 +15,6 @@ // along with Parity. If not, see <http://www.gnu.org/licenses/>. use std::collections::BTreeMap; -use std::sync::RwLock; use std::mem; use ethkey::KeyPair; use crypto::KEY_ITERATIONS; @@ -26,6 +25,7 @@ use account::SafeAccount; use {Error, SecretStore}; use json; use json::UUID; +use parking_lot::RwLock; use presale::PresaleWallet; use import; @@ -56,13 +56,13 @@ impl EthStore { let account = try!(self.dir.insert(account.clone())); // update cache - let mut cache = self.cache.write().unwrap(); + let mut cache = self.cache.write(); cache.insert(account.address.clone(), account); Ok(()) } fn reload_accounts(&self) -> Result<(), Error> { - let mut cache = self.cache.write().unwrap(); + let mut cache = self.cache.write(); let accounts = try!(self.dir.load()); let new_accounts: BTreeMap<_, _> = accounts.into_iter().map(|account| (account.address.clone(), account)).collect(); mem::replace(&mut *cache, new_accounts); @@ -71,13 +71,13 @@ impl EthStore { fn get(&self, address: &Address) -> Result<SafeAccount, Error> { { - let cache = self.cache.read().unwrap(); + let cache = self.cache.read(); if let Some(account) = cache.get(address) { return Ok(account.clone()) } } try!(self.reload_accounts()); - let cache = self.cache.read().unwrap(); + let cache = self.cache.read(); cache.get(address).cloned().ok_or(Error::InvalidAccount) } } @@ -111,7 +111,7 @@ impl SecretStore for EthStore { fn accounts(&self) -> Result<Vec<Address>, Error> { try!(self.reload_accounts()); - Ok(self.cache.read().unwrap().keys().cloned().collect()) + Ok(self.cache.read().keys().cloned().collect()) } fn change_password(&self, address: &Address, old_password: &str, new_password: &str) -> Result<(), Error> { @@ -131,7 +131,7 @@ impl SecretStore for EthStore { if can_remove { try!(self.dir.remove(address)); - let mut cache = self.cache.write().unwrap(); + let mut cache = self.cache.write(); cache.remove(address); Ok(()) } else { diff --git a/ethstore/src/lib.rs b/ethstore/src/lib.rs index 302e165cf..f8619ff19 100644 --- a/ethstore/src/lib.rs +++ b/ethstore/src/lib.rs @@ -26,12 +26,15 @@ extern crate serde_json; extern crate rustc_serialize; extern crate crypto as rcrypto; extern crate tiny_keccak; -#[macro_use] -extern crate lazy_static; +extern crate parking_lot; + // reexport it nicely extern crate ethkey as _ethkey; extern crate ethcrypto as crypto; +#[macro_use] +extern crate lazy_static; + pub mod dir; pub mod ethkey; diff --git a/ethstore/src/presale.rs b/ethstore/src/presale.rs index 2904db6ef..ff3bde6f4 100644 --- a/ethstore/src/presale.rs +++ b/ethstore/src/presale.rs @@ -33,7 +33,8 @@ impl From<json::PresaleWallet> for PresaleWallet { impl PresaleWallet { pub fn open<P>(path: P) -> Result<Self, Error> where P: AsRef<Path> { let file = try!(fs::File::open(path)); - let presale = json::PresaleWallet::load(file).unwrap(); + let presale = try!(json::PresaleWallet::load(file) + .map_err(|e| Error::InvalidKeyFile(format!("{}", e)))); Ok(PresaleWallet::from(presale)) } diff --git a/util/src/common.rs b/util/src/common.rs index 216f89a79..ea2a0f5ea 100644 --- a/util/src/common.rs +++ b/util/src/common.rs @@ -95,8 +95,8 @@ macro_rules! flushln { #[doc(hidden)] pub fn flush(s: String) { - ::std::io::stdout().write(s.as_bytes()).unwrap(); - ::std::io::stdout().flush().unwrap(); + let _ = ::std::io::stdout().write(s.as_bytes()); + let _ = ::std::io::stdout().flush(); } #[test] diff --git a/util/src/journaldb/earlymergedb.rs b/util/src/journaldb/earlymergedb.rs index ef9868d41..0f7c097b8 100644 --- a/util/src/journaldb/earlymergedb.rs +++ b/util/src/journaldb/earlymergedb.rs @@ -383,7 +383,10 @@ impl JournalDB for EarlyMergeDB { let trace = false; // record new commit's details. - let mut refs = self.refs.as_ref().unwrap().write(); + let mut refs = match self.refs.as_ref() { + Some(refs) => refs.write(), + None => return Ok(0), + }; { let mut index = 0usize; diff --git a/util/src/kvdb.rs b/util/src/kvdb.rs index 63d46d573..b37af4dc9 100644 --- a/util/src/kvdb.rs +++ b/util/src/kvdb.rs @@ -345,7 +345,8 @@ impl Database { let cfnames: Vec<&str> = cfnames.iter().map(|n| n as &str).collect(); match DB::open_cf(&opts, path, &cfnames, &cf_options) { Ok(db) => { - cfs = cfnames.iter().map(|n| db.cf_handle(n).unwrap()).collect(); + cfs = cfnames.iter().map(|n| db.cf_handle(n) + .expect("rocksdb opens a cf_handle for each cfname; qed")).collect(); assert!(cfs.len() == columns as usize); Ok(db) } @@ -353,7 +354,7 @@ impl Database { // retry and create CFs match DB::open_cf(&opts, path, &[], &[]) { Ok(mut db) => { - cfs = cfnames.iter().enumerate().map(|(i, n)| db.create_cf(n, &cf_options[i]).unwrap()).collect(); + cfs = try!(cfnames.iter().enumerate().map(|(i, n)| db.create_cf(n, &cf_options[i])).collect()); Ok(db) }, err @ Err(_) => err, @@ -537,7 +538,8 @@ impl Database { match *self.db.read() { Some(DBAndColumns { ref db, ref cfs }) => { let mut iter = col.map_or_else(|| db.iterator_opt(IteratorMode::From(prefix, Direction::Forward), &self.read_opts), - |c| db.iterator_cf_opt(cfs[c as usize], IteratorMode::From(prefix, Direction::Forward), &self.read_opts).unwrap()); + |c| db.iterator_cf_opt(cfs[c as usize], IteratorMode::From(prefix, Direction::Forward), &self.read_opts) + .expect("iterator params are valid; qed")); match iter.next() { // TODO: use prefix_same_as_start read option (not availabele in C API currently) Some((k, v)) => if k[0 .. prefix.len()] == prefix[..] { Some(v) } else { None }, @@ -554,7 +556,8 @@ impl Database { match *self.db.read() { Some(DBAndColumns { ref db, ref cfs }) => { col.map_or_else(|| DatabaseIterator { iter: db.iterator_opt(IteratorMode::Start, &self.read_opts) }, - |c| DatabaseIterator { iter: db.iterator_cf_opt(cfs[c as usize], IteratorMode::Start, &self.read_opts).unwrap() }) + |c| DatabaseIterator { iter: db.iterator_cf_opt(cfs[c as usize], IteratorMode::Start, &self.read_opts) + .expect("iterator params are valid; qed") }) }, None => panic!("Not supported yet") //TODO: return an empty iterator or change return type } diff --git a/util/src/memorydb.rs b/util/src/memorydb.rs index 36e3fad1b..d0ecb73d9 100644 --- a/util/src/memorydb.rs +++ b/util/src/memorydb.rs @@ -148,7 +148,7 @@ impl MemoryDB { (*p).insert(key.clone(), (value, 0)); } } - self.raw(key).unwrap() + self.raw(key).expect("entry just inserted into data; qed") } /// Returns the size of allocated heap memory diff --git a/util/src/trie/triedb.rs b/util/src/trie/triedb.rs index 5522af448..72604892d 100644 --- a/util/src/trie/triedb.rs +++ b/util/src/trie/triedb.rs @@ -296,7 +296,7 @@ impl<'a> TrieDBIterator<'a> { status: Status::Entering, node: try!(self.db.get_node(d, &mut NoOp, 0)), }); - match self.trail.last().unwrap().node { + match self.trail.last().expect("just pushed item; qed").node { Node::Leaf(n, _) | Node::Extension(n, _) => { self.key_nibbles.extend(n.iter()); }, _ => {} } @@ -346,7 +346,8 @@ impl<'a> Iterator for TrieDBIterator<'a> { (Status::AtChild(i), Node::Branch(children, _)) if children[i].len() > 0 => { match i { 0 => self.key_nibbles.push(0), - i => *self.key_nibbles.last_mut().unwrap() = i as u8, + i => *self.key_nibbles.last_mut() + .expect("pushed as 0; moves sequentially; removed afterwards; qed") = i as u8, } if let Err(e) = self.descend(children[i]) { return Some(Err(e)); diff --git a/util/src/trie/triedbmut.rs b/util/src/trie/triedbmut.rs index 5e2ef6b79..e99179e7a 100644 --- a/util/src/trie/triedbmut.rs +++ b/util/src/trie/triedbmut.rs @@ -744,7 +744,8 @@ impl<'a> TrieDBMut<'a> { (UsedIndex::One(a), None) => { // only one onward node. make an extension. let new_partial = NibbleSlice::new_offset(&[a], 1).encoded(false); - let new_node = Node::Extension(new_partial, children[a as usize].take().unwrap()); + let child = children[a as usize].take().expect("used_index only set if occupied; qed"); + let new_node = Node::Extension(new_partial, child); self.fix(new_node) } (UsedIndex::None, Some(value)) => { From 20d43b9c26328d6a81d5939bcb1870656e5b377d Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Tue, 25 Oct 2016 20:37:47 +0000 Subject: [PATCH 53/77] [ci skip] js-precompiled 20161025-203648 --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index d6ccbf86f..31e91ae87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1208,7 +1208,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#18cc1f1aba75b9a7556f0461fb9379dbd0a34f02" +source = "git+https://github.com/ethcore/js-precompiled.git#1a88fbdd3b8eba7aea0db1427e7756455fefa09f" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] From 51a78d290eb192e6c00d41f79c32bcdb9f247638 Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" <general.beck@gmail.com> Date: Wed, 26 Oct 2016 10:57:35 +0700 Subject: [PATCH 54/77] Update gitlab-ci add arm* bulds add deb packages (amd64,arm64,armhf) --- .gitlab-ci.yml | 60 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7a13531b3..0cc444846 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ stages: - test variables: GIT_DEPTH: "3" - SIMPLECOV: "true" + SIMPLECOV: "true" RUST_BACKTRACE: "1" RUSTFLAGS: "-D warnings" cache: @@ -21,10 +21,17 @@ linux-stable: - cargo build --release --verbose - strip target/release/parity - md5sum target/release/parity >> parity.md5 - - aws configure set aws_access_key_id $s3_key + - sh scripts/deb-build.sh amd64 + - cp target/release/parity deb/usr/bin/parity + - export VER=$(grep -m 1 version Cargo.toml | awk '{print $3}' | tr -d '"' | tr -d "\n") + - dpkg-deb -b deb "parity_"$VER"_amd64.deb" + - md5sum "parity_"$VER"_amd64.deb" >> "parity_"$VER"_amd64.deb.md5" + - aws configure set aws_access_key_id $s3_key - aws configure set aws_secret_access_key $s3_secret - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu/parity --body target/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu/parity.md5 --body parity.md5 + - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu/"parity_"$VER"_amd64.deb" --body "parity_"$VER"_amd64.deb" + - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-linux-gnu/"parity_"$VER"_amd64.deb.md5" --body "parity_"$VER"_amd64.deb.md5" tags: - rust - rust-stable @@ -44,10 +51,17 @@ linux-stable-14.04: - cargo build --release --verbose - strip target/release/parity - md5sum target/release/parity >> parity.md5 - - aws configure set aws_access_key_id $s3_key + - sh scripts/deb-build.sh amd64 + - cp target/release/parity deb/usr/bin/parity + - export VER=$(grep -m 1 version Cargo.toml | awk '{print $3}' | tr -d '"' | tr -d "\n") + - dpkg-deb -b deb "parity_"$VER"_amd64.deb" + - md5sum "parity_"$VER"_amd64.deb" >> "parity_"$VER"_amd64.deb.md5" + - aws configure set aws_access_key_id $s3_key - aws configure set aws_secret_access_key $s3_secret - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-ubuntu_14_04-gnu/parity --body target/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-ubuntu_14_04-gnu/parity.md5 --body parity.md5 + - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-ubuntu_14_04-gnu/"parity_"$VER"_amd64.deb" --body "parity_"$VER"_amd64.deb" + - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-ubuntu_14_04-gnu/"parity_"$VER"_amd64.deb.md5" --body "parity_"$VER"_amd64.deb.md5" tags: - rust - rust-14.04 @@ -107,7 +121,7 @@ linux-centos: - cargo build --release --verbose - strip target/release/parity - md5sum target/release/parity >> parity.md5 - - aws configure set aws_access_key_id $s3_key + - aws configure set aws_access_key_id $s3_key - aws configure set aws_secret_access_key $s3_secret - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-centos-gnu/parity --body target/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-unknown-centos-gnu/parity.md5 --body parity.md5 @@ -134,10 +148,17 @@ linux-armv7: - cargo build --target armv7-unknown-linux-gnueabihf --release --verbose - arm-linux-gnueabihf-strip target/armv7-unknown-linux-gnueabihf/release/parity - md5sum target/armv7-unknown-linux-gnueabihf/release/parity >> parity.md5 - - aws configure set aws_access_key_id $s3_key + - sh scripts/deb-build.sh armhf + - cp target/armv7-unknown-linux-gnueabihf/release/parity deb/usr/bin/parity + - export VER=$(grep -m 1 version Cargo.toml | awk '{print $3}' | tr -d '"' | tr -d "\n") + - dpkg-deb -b deb "parity_"$VER"_armhf.deb" + - md5sum "parity_"$VER"_armhf.deb" >> "parity_"$VER"_armhf.deb.md5" + - aws configure set aws_access_key_id $s3_key - aws configure set aws_secret_access_key $s3_secret - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/armv7-unknown-linux-gnueabihf/parity --body target/armv7-unknown-linux-gnueabihf/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/armv7-unknown-linux-gnueabihf/parity.md5 --body parity.md5 + - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/armv7-unknown-linux-gnueabihf/"parity_"$VER"_armhf.deb" --body "parity_"$VER"_armhf.deb" + - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/armv7-unknown-linux-gnueabihf/"parity_"$VER"_armhf.deb.md5" --body "parity_"$VER"_armhf.deb.md5" tags: - rust - rust-arm @@ -162,10 +183,17 @@ linux-arm: - cargo build --target arm-unknown-linux-gnueabihf --release --verbose - arm-linux-gnueabihf-strip target/arm-unknown-linux-gnueabihf/release/parity - md5sum target/arm-unknown-linux-gnueabihf/release/parity >> parity.md5 - - aws configure set aws_access_key_id $s3_key + - sh scripts/deb-build.sh armhf + - cp target/arm-unknown-linux-gnueabihf/release/parity deb/usr/bin/parity + - export VER=$(grep -m 1 version Cargo.toml | awk '{print $3}' | tr -d '"' | tr -d "\n") + - dpkg-deb -b deb "parity_"$VER"_armhf.deb" + - md5sum "parity_"$VER"_armhf.deb" >> "parity_"$VER"_armhf.deb.md5" + - aws configure set aws_access_key_id $s3_key - aws configure set aws_secret_access_key $s3_secret - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/arm-unknown-linux-gnueabihf/parity --body target/arm-unknown-linux-gnueabihf/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/arm-unknown-linux-gnueabihf/parity.md5 --body parity.md5 + - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/arm-unknown-linux-gnueabihf/"parity_"$VER"_armhf.deb" --body "parity_"$VER"_armhf.deb" + - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/arm-unknown-linux-gnueabihf/"parity_"$VER"_armhf.deb.md5" --body "parity_"$VER"_armhf.deb.md5" tags: - rust - rust-arm @@ -189,8 +217,8 @@ linux-armv6: - cat .cargo/config - cargo build --target arm-unknown-linux-gnueabi --release --verbose - arm-linux-gnueabi-strip target/arm-unknown-linux-gnueabi/release/parity - - md5sum target/arm-unknown-linux-gnueabi/release/parity >> parity.md5 - - aws configure set aws_access_key_id $s3_key + - md5sum target/arm-unknown-linux-gnueabi/release/parity >> parity.md5 + - aws configure set aws_access_key_id $s3_key - aws configure set aws_secret_access_key $s3_secret - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/arm-unknown-linux-gnueabi/parity --body target/arm-unknown-linux-gnueabi/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/arm-unknown-linux-gnueabi/parity.md5 --body parity.md5 @@ -218,10 +246,17 @@ linux-aarch64: - cargo build --target aarch64-unknown-linux-gnu --release --verbose - aarch64-linux-gnu-strip target/aarch64-unknown-linux-gnu/release/parity - md5sum target/aarch64-unknown-linux-gnu/release/parity >> parity.md5 - - aws configure set aws_access_key_id $s3_key + - sh scripts/deb-build.sh arm64 + - cp target/aarch64-unknown-linux-gnu/release/parity deb/usr/bin/parity + - export VER=$(grep -m 1 version Cargo.toml | awk '{print $3}' | tr -d '"' | tr -d "\n") + - dpkg-deb -b deb "parity_"$VER"_arm64.deb" + - md5sum "parity_"$VER"_arm64.deb" >> "parity_"$VER"_arm64.deb.md5" + - aws configure set aws_access_key_id $s3_key - aws configure set aws_secret_access_key $s3_secret - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/aarch64-unknown-linux-gnu/parity --body target/aarch64-unknown-linux-gnu/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/aarch64-unknown-linux-gnu/parity.md5 --body parity.md5 + - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/aarch64-unknown-linux-gnu/"parity_"$VER"_arm64.deb" --body "parity_"$VER"_arm64.deb" + - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/aarch64-unknown-linux-gnu/"parity_"$VER"_arm64.deb.md5" --body "parity_"$VER"_arm64.deb.md5" tags: - rust - rust-arm @@ -240,7 +275,7 @@ darwin: script: - cargo build --release --verbose - md5sum target/release/parity >> parity.md5 - - aws configure set aws_access_key_id $s3_key + - aws configure set aws_access_key_id $s3_key - aws configure set aws_secret_access_key $s3_secret - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-apple-darwin/parity --body target/release/parity - aws s3api put-object --bucket builds-parity --key $CI_BUILD_REF_NAME/x86_64-apple-darwin/parity.md5 --body parity.md5 @@ -261,10 +296,8 @@ windows: - set INCLUDE=C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Include;C:\vs2015\VC\include;C:\Program Files (x86)\Windows Kits\10\Include\10.0.10240.0\ucrt - set LIB=C:\vs2015\VC\lib;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10240.0\ucrt\x64 - set RUST_BACKTRACE=1 - - set RUSTFLAGS=-Zorbit=off - - set RUSTFLAGS=-D warnings + - set RUSTFLAGS=%RUSTFLAGS% -Zorbit=off -D warnings - rustup default stable-x86_64-pc-windows-msvc - - git submodule update --init - cargo build --release --verbose - curl -sL --url "https://github.com/ethcore/win-build/raw/master/SimpleFC.dll" -o nsis\SimpleFC.dll - curl -sL --url "https://github.com/ethcore/win-build/raw/master/vc_redist.x64.exe" -o nsis\vc_redist.x64.exe @@ -300,7 +333,6 @@ windows: - target/release/parity.pdb - nsis/InstallParity.exe name: "x86_64-pc-windows-msvc_parity" - allow_failure: true test-linux: stage: test before_script: From 7b7ce4a0b1f5fbfdcd193804ccdfaf825042eafc Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Wed, 26 Oct 2016 04:08:54 +0000 Subject: [PATCH 55/77] [ci skip] js-precompiled 20161026-040753 --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 31e91ae87..b067ce305 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1208,7 +1208,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#1a88fbdd3b8eba7aea0db1427e7756455fefa09f" +source = "git+https://github.com/ethcore/js-precompiled.git#afc53a95c47798b7e94d5900070d062e6fb6c749" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] From 70d6ad6682bcb0a34099b47d7d32f13d0ec731b2 Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" <general.beck@gmail.com> Date: Wed, 26 Oct 2016 11:36:07 +0700 Subject: [PATCH 56/77] Create deb-build.sh [ci skip] add sh for deb build --- scripts/deb-build.sh | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 scripts/deb-build.sh diff --git a/scripts/deb-build.sh b/scripts/deb-build.sh new file mode 100644 index 000000000..73e236db5 --- /dev/null +++ b/scripts/deb-build.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +set -e # fail on any error +set -u # treat unset variables as error +rm -rf deb +#create DEBIAN files +mkdir -p deb/usr/bin/ +mkdir -p deb/DEBIAN +#create copyright, docs, compat +cp LICENSE deb/DEBIAN/copyright +echo "https://github.com/ethcore/parity/wiki" >> deb/DEBIAN/docs +echo "8" >> deb/DEBIAN/compat +#create control file +control=deb/DEBIAN/control +echo "Package: parity" >> $control +version=`grep -m 1 version Cargo.toml | awk '{print $3}' | tr -d '"' | tr -d "\n"` +echo "Version: $version" >> $control +echo "Source: parity" >> $control +echo "Section: science" >> $control +echo "Priority: extra" >> $control +echo "Maintainer: Ethcore <admin@ethcore.io>" >> $control +echo "Build-Depends: debhelper (>=9)" >> $control +echo "Standards-Version: 3.9.5" >> $control +echo "Homepage: https://ethcore.io" >> $control +echo "Vcs-Git: git://github.com/ethcore/parity.git" >> $control +echo "Vcs-Browser: https://github.com/ethcore/parity" >> $control +echo "Architecture: $1" >> $control +echo "Description: Ethereum network client by Ethcore" >> $control +#build .deb package + +exit From d28af0a3ab5e12fcfa05f9c4d8cae8558ae479fb Mon Sep 17 00:00:00 2001 From: "Denis S. Soldatov aka General-Beck" <general.beck@gmail.com> Date: Wed, 26 Oct 2016 11:41:54 +0700 Subject: [PATCH 57/77] Update deb-build.sh change email --- scripts/deb-build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deb-build.sh b/scripts/deb-build.sh index 73e236db5..9754f1b05 100644 --- a/scripts/deb-build.sh +++ b/scripts/deb-build.sh @@ -18,7 +18,7 @@ echo "Version: $version" >> $control echo "Source: parity" >> $control echo "Section: science" >> $control echo "Priority: extra" >> $control -echo "Maintainer: Ethcore <admin@ethcore.io>" >> $control +echo "Maintainer: Ethcore <devops@ethcore.io>" >> $control echo "Build-Depends: debhelper (>=9)" >> $control echo "Standards-Version: 3.9.5" >> $control echo "Homepage: https://ethcore.io" >> $control From 162d1a032bc4ea66b0ad3072e0000a08cafe4f8e Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Wed, 26 Oct 2016 04:44:46 +0000 Subject: [PATCH 58/77] [ci skip] js-precompiled 20161026-044349 --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index b067ce305..0a7cb861f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1208,7 +1208,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#afc53a95c47798b7e94d5900070d062e6fb6c749" +source = "git+https://github.com/ethcore/js-precompiled.git#76a1719d46c57575a62bdb8483748a49fc6f235e" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] From e1c2cff9573527ba4fb734abaa3233d6fa9e48bc Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Wed, 26 Oct 2016 10:41:46 +0200 Subject: [PATCH 59/77] fix failing tests after log parsing updates --- js/src/api/contract/contract.spec.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/js/src/api/contract/contract.spec.js b/js/src/api/contract/contract.spec.js index 7ac3e099c..0d6169e26 100644 --- a/js/src/api/contract/contract.spec.js +++ b/js/src/api/contract/contract.spec.js @@ -173,12 +173,12 @@ describe('api/contract/Contract', () => { expect(log.event).to.equal('Message'); expect(log.address).to.equal('0x22bff18ec62281850546a664bb63a5c06ac5f76c'); expect(log.params).to.deep.equal({ - at: new BigNumber('1457965151'), - message: 'post(message)', - messageId: new BigNumber('281474976731085'), - parentId: new BigNumber(0), - postId: new BigNumber('281474976731104'), - sender: '0x63Cf90D3f0410092FC0fca41846f596223979195' + at: { type: 'uint', value: new BigNumber('1457965151') }, + message: { type: 'string', value: 'post(message)' }, + messageId: { type: 'uint', value: new BigNumber('281474976731085') }, + parentId: { type: 'uint', value: new BigNumber(0) }, + postId: { type: 'uint', value: new BigNumber('281474976731104') }, + sender: { type: 'address', value: '0x63Cf90D3f0410092FC0fca41846f596223979195' } }); }); }); @@ -464,12 +464,12 @@ describe('api/contract/Contract', () => { event: 'Message', logIndex: new BigNumber(0), params: { - at: new BigNumber(1457965151), - message: 'post(message)', - messageId: new BigNumber(281474976731085), - parentId: new BigNumber(0), - postId: new BigNumber(281474976731104), - sender: '0x63Cf90D3f0410092FC0fca41846f596223979195' + at: { type: 'uint', value: new BigNumber(1457965151) }, + message: { type: 'string', value: 'post(message)' }, + messageId: { type: 'uint', value: new BigNumber(281474976731085) }, + parentId: { type: 'uint', value: new BigNumber(0) }, + postId: { type: 'uint', value: new BigNumber(281474976731104) }, + sender: { type: 'address', value: '0x63Cf90D3f0410092FC0fca41846f596223979195' } }, topics: [ '0x954ba6c157daf8a26539574ffa64203c044691aa57251af95f4b48d85ec00dd5', '0x0000000000000000000000000000000000000000000000000001000000004fe0' From 3cd724d056b2455b8f696a02992c149348c2f55b Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Wed, 26 Oct 2016 09:56:14 +0000 Subject: [PATCH 60/77] [ci skip] js-precompiled 20161026-095510 --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 0a7cb861f..2e490d36c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1208,7 +1208,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#76a1719d46c57575a62bdb8483748a49fc6f235e" +source = "git+https://github.com/ethcore/js-precompiled.git#b906f490151d605feb37b2c4def1533bf137f76d" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] From c05430e25e1e9b2e68998156f98cc2461064db9e Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac <ngotchac@gmail.com> Date: Wed, 26 Oct 2016 11:56:38 +0200 Subject: [PATCH 61/77] Fixes signer errors throwing // MaterialUI Update errors too (#2867) (#2876) --- js/src/api/transport/ws/ws.js | 35 ++++++++++++------- js/src/dapps/gavcoin/Loading/loading.js | 2 +- .../dapps/registry/Application/application.js | 2 +- js/src/dapps/tokenreg/Loading/loading.js | 4 ++- js/src/ui/Container/container.css | 4 ++- js/src/views/ParityBar/parityBar.css | 3 +- .../TransactionFinished.js | 2 +- .../TransactionPending/TransactionPending.js | 2 +- 8 files changed, 34 insertions(+), 20 deletions(-) diff --git a/js/src/api/transport/ws/ws.js b/js/src/api/transport/ws/ws.js index ecab2a5a2..119f4ba76 100644 --- a/js/src/api/transport/ws/ws.js +++ b/js/src/api/transport/ws/ws.js @@ -93,21 +93,30 @@ export default class Ws extends JsonRpcBase { } _onMessage = (event) => { - const result = JSON.parse(event.data); - const { method, params, json, resolve, reject } = this._messages[result.id]; - - Logging.send(method, params, { json, result }); - - if (result.error) { - this.error(event.data); - - reject(new Error(`${result.error.code}: ${result.error.message}`)); - delete this._messages[result.id]; - return; + // Event sent by Signer Broadcaster + if (event.data === 'new_message') { + return false; } - resolve(result.result); - delete this._messages[result.id]; + try { + const result = JSON.parse(event.data); + const { method, params, json, resolve, reject } = this._messages[result.id]; + + Logging.send(method, params, { json, result }); + + if (result.error) { + this.error(event.data); + + reject(new Error(`${result.error.code}: ${result.error.message}`)); + delete this._messages[result.id]; + return; + } + + resolve(result.result); + delete this._messages[result.id]; + } catch (e) { + console.error('ws::_onMessage', event.data, e); + } } _send = (id) => { diff --git a/js/src/dapps/gavcoin/Loading/loading.js b/js/src/dapps/gavcoin/Loading/loading.js index 9e00cc8f1..78aaa8828 100644 --- a/js/src/dapps/gavcoin/Loading/loading.js +++ b/js/src/dapps/gavcoin/Loading/loading.js @@ -24,7 +24,7 @@ export default class Loading extends Component { render () { return ( <div className={ styles.loading }> - <CircularProgress size={ 2 } /> + <CircularProgress size={ 120 } thickness={ 7 } /> </div> ); } diff --git a/js/src/dapps/registry/Application/application.js b/js/src/dapps/registry/Application/application.js index 3d3d8d582..d0c4bd2f7 100644 --- a/js/src/dapps/registry/Application/application.js +++ b/js/src/dapps/registry/Application/application.js @@ -79,7 +79,7 @@ export default class Application extends Component { </p> </div> ) : ( - <CircularProgress size={ 1 } /> + <CircularProgress size={ 60 } /> ) } </div> ); diff --git a/js/src/dapps/tokenreg/Loading/loading.js b/js/src/dapps/tokenreg/Loading/loading.js index 3b7619323..bdcc98df6 100644 --- a/js/src/dapps/tokenreg/Loading/loading.js +++ b/js/src/dapps/tokenreg/Loading/loading.js @@ -25,9 +25,11 @@ export default class Loading extends Component { }; render () { + const size = (this.props.size || 2) * 60; + return ( <div className={ styles.loading }> - <CircularProgress size={ this.props.size || 2 } /> + <CircularProgress size={ size } /> </div> ); } diff --git a/js/src/ui/Container/container.css b/js/src/ui/Container/container.css index ad625aa7c..7305a4aba 100644 --- a/js/src/ui/Container/container.css +++ b/js/src/ui/Container/container.css @@ -15,15 +15,17 @@ /* along with Parity. If not, see <http://www.gnu.org/licenses/>. */ .container { + flex: 1; padding: 0em; + background: rgba(0, 0, 0, 0.8) !important; } .compact, .padded { - background: rgba(0, 0, 0, 0.8) !important; border-radius: 0 !important; position: relative; overflow: auto; + background-color: transparent !important; } .compact { diff --git a/js/src/views/ParityBar/parityBar.css b/js/src/views/ParityBar/parityBar.css index 0c6de9f5d..b9b94c1d2 100644 --- a/js/src/views/ParityBar/parityBar.css +++ b/js/src/views/ParityBar/parityBar.css @@ -43,7 +43,7 @@ .expanded { right: 16px; width: 964px; - height: 288px; + height: 300px; border-radius: 4px 4px 0 0; overflow-y: auto; display: flex; @@ -53,6 +53,7 @@ .expanded .content { flex: 1; overflow: auto; + display: flex; } .corner { diff --git a/js/src/views/Signer/components/TransactionFinished/TransactionFinished.js b/js/src/views/Signer/components/TransactionFinished/TransactionFinished.js index d7717c7fb..9326f57e6 100644 --- a/js/src/views/Signer/components/TransactionFinished/TransactionFinished.js +++ b/js/src/views/Signer/components/TransactionFinished/TransactionFinished.js @@ -80,7 +80,7 @@ export default class TransactionFinished extends Component { if (!chain || !fromBalance || !toBalance) { return ( <div className={ `${styles.container} ${className}` }> - <CircularProgress size={ 1 } /> + <CircularProgress size={ 60 } /> </div> ); } diff --git a/js/src/views/Signer/components/TransactionPending/TransactionPending.js b/js/src/views/Signer/components/TransactionPending/TransactionPending.js index 9f77e6cc1..3ca078b83 100644 --- a/js/src/views/Signer/components/TransactionPending/TransactionPending.js +++ b/js/src/views/Signer/components/TransactionPending/TransactionPending.js @@ -81,7 +81,7 @@ export default class TransactionPending extends Component { if (!this.state.chain) { return ( <div className={ `${styles.container} ${className}` }> - <CircularProgress size={ 1 } /> + <CircularProgress size={ 60 } /> </div> ); } From 71e973cb0b4b4c9fc4f294b384465de6abf787b5 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Wed, 26 Oct 2016 10:05:40 +0000 Subject: [PATCH 62/77] [ci skip] js-precompiled 20161026-100446 --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 2e490d36c..27ac800f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1208,7 +1208,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#b906f490151d605feb37b2c4def1533bf137f76d" +source = "git+https://github.com/ethcore/js-precompiled.git#6be42e2bcf15db125797097df7a2dcfbf7d1e1d2" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] From c8809b3396ff3295ccd3e1934277a62c686fc9e8 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Wed, 26 Oct 2016 13:44:38 +0200 Subject: [PATCH 63/77] Add inject to "bundle everything" list (#2871) * Add inject to "bundle everything" list * Fixes the `build-server` script // Updates Webpack config (#2872) * New Webpack config file for libraries * Added `parity-utils` path * Removed parity in CommonChunks prod --- js/build-server.js | 25 +++++++++- js/package.json | 17 ++++--- js/src/dapps/basiccoin.html | 2 +- js/src/dapps/gavcoin.html | 2 +- js/src/dapps/githubhint.html | 2 +- js/src/dapps/registry.html | 2 +- js/src/dapps/signaturereg.html | 2 +- js/src/dapps/tokenreg.html | 2 +- js/src/dev.parity.html | 2 +- js/src/dev.web3.html | 2 +- js/src/environment/index.js | 8 ++- js/src/{inject.js => web3.js} | 0 js/webpack.config.js | 27 +++++------ js/webpack.libraries.js | 89 ++++++++++++++++++++++++++++++++++ 14 files changed, 151 insertions(+), 31 deletions(-) rename js/src/{inject.js => web3.js} (100%) create mode 100644 js/webpack.libraries.js diff --git a/js/build-server.js b/js/build-server.js index 797e89183..9153f5ed2 100644 --- a/js/build-server.js +++ b/js/build-server.js @@ -24,17 +24,38 @@ var express = require('express'); var proxy = require('http-proxy-middleware'); var app = express(); +var wsProxy = proxy('ws://127.0.0.1:8180', { changeOrigin: true }); -app.use(express.static('build')); +app.use(express.static('.build')); app.use('/api/*', proxy({ target: 'http://127.0.0.1:8080', changeOrigin: true })); +app.use('/app/*', proxy({ + target: 'http://127.0.0.1:8080', + changeOrigin: true, + pathRewrite: { + '^/app': '' + } +})); + +app.use('/parity-utils/*', proxy({ + target: 'http://127.0.0.1:3000', + changeOrigin: true, + pathRewrite: { + '^/parity-utils': '' + } +})); + app.use('/rpc/*', proxy({ target: 'http://127.0.0.1:8080', changeOrigin: true })); -app.listen(3000); +app.use(wsProxy); + +var server = app.listen(3000); + +server.on('upgrade', wsProxy.upgrade); diff --git a/js/package.json b/js/package.json index d84b21d25..8992c7fa6 100644 --- a/js/package.json +++ b/js/package.json @@ -23,17 +23,22 @@ "Promise" ], "scripts": { - "build": "npm run build:dll && npm run build:app", + "build": "npm run build:dll && npm run build:app && npm run build:lib", "build:app": "webpack --progress", - "build:dll": "webpack --config webpack.vendor.js --progress", - "ci:build": "npm run ci:build:dll && npm run ci:build:app", + "build:lib": "webpack --config webpack.libraries --progress", + "build:dll": "webpack --config webpack.vendor --progress", + + "ci:build": "npm run ci:build:dll && npm run ci:build:dll && npm run ci:build:app", "ci:build:app": "NODE_ENV=production webpack", - "ci:build:dll": "NODE_ENV=production webpack --config webpack.vendor.js", + "ci:build:lib": "NODE_ENV=production webpack --config webpack.libraries", + "ci:build:dll": "NODE_ENV=production webpack --config webpack.vendor", + + "start": "npm install && npm run build:dll && npm run start:app", + "start:app": "webpack-dev-server -d --history-api-fallback --open --hot --inline --progress --colors --port 3000", + "clean": "rm -rf ./build ./coverage", "coveralls": "npm run testCoverage && coveralls < coverage/lcov.info", "lint": "eslint --ignore-path .gitignore ./src/", - "start": "npm install && npm run build:dll && npm run start:app", - "start:app": "webpack-dev-server -d --history-api-fallback --open --hot --inline --progress --colors --port 3000", "test": "mocha 'src/**/*.spec.js'", "test:coverage": "istanbul cover _mocha -- 'src/**/*.spec.js'", "test:e2e": "mocha 'src/**/*.e2e.js'" diff --git a/js/src/dapps/basiccoin.html b/js/src/dapps/basiccoin.html index 9bcc368f3..7ac5cb3cb 100644 --- a/js/src/dapps/basiccoin.html +++ b/js/src/dapps/basiccoin.html @@ -10,7 +10,7 @@ <div id="container"></div> <script src="vendor.js"></script> <script src="commons.js"></script> - <script src="parity.js"></script> + <script src="/parity-utils/parity.js"></script> <script src="basiccoin.js"></script> </body> </html> diff --git a/js/src/dapps/gavcoin.html b/js/src/dapps/gavcoin.html index 928310a52..f777f2920 100644 --- a/js/src/dapps/gavcoin.html +++ b/js/src/dapps/gavcoin.html @@ -10,7 +10,7 @@ <div id="container"></div> <script src="vendor.js"></script> <script src="commons.js"></script> - <script src="parity.js"></script> + <script src="/parity-utils/parity.js"></script> <script src="gavcoin.js"></script> </body> </html> diff --git a/js/src/dapps/githubhint.html b/js/src/dapps/githubhint.html index 0084dd051..085b15953 100644 --- a/js/src/dapps/githubhint.html +++ b/js/src/dapps/githubhint.html @@ -10,7 +10,7 @@ <div id="container"></div> <script src="vendor.js"></script> <script src="commons.js"></script> - <script src="parity.js"></script> + <script src="/parity-utils/parity.js"></script> <script src="githubhint.js"></script> </body> </html> diff --git a/js/src/dapps/registry.html b/js/src/dapps/registry.html index 21b09dc12..83c5e8c9b 100644 --- a/js/src/dapps/registry.html +++ b/js/src/dapps/registry.html @@ -10,7 +10,7 @@ <div id="container"></div> <script src="vendor.js"></script> <script src="commons.js"></script> - <script src="parity.js"></script> + <script src="/parity-utils/parity.js"></script> <script src="registry.js"></script> </body> </html> diff --git a/js/src/dapps/signaturereg.html b/js/src/dapps/signaturereg.html index 3f74be28a..be62400d2 100644 --- a/js/src/dapps/signaturereg.html +++ b/js/src/dapps/signaturereg.html @@ -10,7 +10,7 @@ <div id="container"></div> <script src="vendor.js"></script> <script src="commons.js"></script> - <script src="parity.js"></script> + <script src="/parity-utils/parity.js"></script> <script src="signaturereg.js"></script> </body> </html> diff --git a/js/src/dapps/tokenreg.html b/js/src/dapps/tokenreg.html index ecb03d005..bcf04e298 100644 --- a/js/src/dapps/tokenreg.html +++ b/js/src/dapps/tokenreg.html @@ -10,7 +10,7 @@ <div id="container"></div> <script src="vendor.js"></script> <script src="commons.js"></script> - <script src="parity.js"></script> + <script src="/parity-utils/parity.js"></script> <script src="tokenreg.js"></script> </body> </html> diff --git a/js/src/dev.parity.html b/js/src/dev.parity.html index 20b8e965f..56811f7c2 100644 --- a/js/src/dev.parity.html +++ b/js/src/dev.parity.html @@ -7,6 +7,6 @@ <title>dev::Parity.js - + diff --git a/js/src/dev.web3.html b/js/src/dev.web3.html index 97a47eb72..e55e0109b 100644 --- a/js/src/dev.web3.html +++ b/js/src/dev.web3.html @@ -7,6 +7,6 @@ dev::Web3 - + diff --git a/js/src/environment/index.js b/js/src/environment/index.js index 1dfda77aa..9b95bb0da 100644 --- a/js/src/environment/index.js +++ b/js/src/environment/index.js @@ -19,7 +19,13 @@ import './tests'; -const parityNode = process.env.NODE_ENV === 'production' ? 'http://127.0.0.1:8080' : ''; +const parityNode = ( + process.env.PARITY_URL && `http://${process.env.PARITY_URL}` + ) || ( + process.env.NODE_ENV === 'production' + ? 'http://127.0.0.1:8080' + : '' + ); export { parityNode diff --git a/js/src/inject.js b/js/src/web3.js similarity index 100% rename from js/src/inject.js rename to js/src/web3.js diff --git a/js/webpack.config.js b/js/webpack.config.js index 39415b1a1..c72ef8936 100644 --- a/js/webpack.config.js +++ b/js/webpack.config.js @@ -41,8 +41,9 @@ module.exports = { 'signaturereg': ['./dapps/signaturereg.js'], 'tokenreg': ['./dapps/tokenreg.js'], // library - 'inject': ['./inject.js'], 'parity': ['./parity.js'], + 'inject': ['./web3.js'], + 'web3': ['./web3.js'], // app 'index': ['./index.js'] }, @@ -136,10 +137,6 @@ module.exports = { 'babel?cacheDirectory=true' ] }), - new webpack.DllReferencePlugin({ - context: '.', - manifest: require(`./${DEST}/vendor-manifest.json`) - }), new CopyWebpackPlugin([{ from: './error_pages.css', to: 'styles.css' }], {}), new WebpackErrorNotificationPlugin(), new webpack.DefinePlugin({ @@ -149,6 +146,11 @@ module.exports = { PARITY_URL: JSON.stringify(process.env.PARITY_URL), LOGGING: JSON.stringify(!isProd) } + }), + + new webpack.DllReferencePlugin({ + context: '.', + manifest: require(`./${DEST}/vendor-manifest.json`) }) ]; @@ -164,16 +166,10 @@ module.exports = { if (isProd) { plugins.push( new webpack.optimize.CommonsChunkPlugin({ - chunks: [ 'index' ], + chunks: ['index'], name: 'commons' }) ); - plugins.push( - new webpack.optimize.CommonsChunkPlugin({ - chunks: [ 'parity' ], - name: 'parity' - }) - ); plugins.push(new webpack.optimize.OccurrenceOrderPlugin(false)); plugins.push(new webpack.optimize.DedupePlugin()); @@ -208,8 +204,11 @@ module.exports = { } }, '/parity-utils/*': { - target: 'http://127.0.0.1:8080', - changeOrigin: true + target: 'http://127.0.0.1:3000', + changeOrigin: true, + pathRewrite: { + '^/parity-utils': '' + } }, '/rpc/*': { target: 'http://localhost:8080', diff --git a/js/webpack.libraries.js b/js/webpack.libraries.js new file mode 100644 index 000000000..bf54a933f --- /dev/null +++ b/js/webpack.libraries.js @@ -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 . + +// Run with `webpack --config webpack.libraries.js --progress` + +const HappyPack = require('happypack'); +const path = require('path'); +const webpack = require('webpack'); + +const ENV = process.env.NODE_ENV || 'development'; +const isProd = ENV === 'production'; +const DEST = process.env.BUILD_DEST || '.build'; + +module.exports = { + context: path.join(__dirname, './src'), + entry: { + // library + 'inject': ['./web3.js'], + 'web3': ['./web3.js'], + 'parity': ['./parity.js'] + }, + output: { + path: path.join(__dirname, DEST), + filename: '[name].js' + }, + module: { + loaders: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'happypack/loader?id=js' + }, + { + test: /\.json$/, + loaders: ['json'] + }, + { + test: /\.html$/, + loader: 'file?name=[name].[ext]' + } + ] + }, + plugins: (function () { + const plugins = [ + new HappyPack({ + id: 'js', + threads: 4, + loaders: [ 'babel' ] + }), + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify(ENV), + RPC_ADDRESS: JSON.stringify(process.env.RPC_ADDRESS), + PARITY_URL: JSON.stringify(process.env.PARITY_URL), + LOGGING: JSON.stringify(!isProd) + } + }) + ]; + + if (isProd) { + plugins.push(new webpack.optimize.OccurrenceOrderPlugin(false)); + plugins.push(new webpack.optimize.DedupePlugin()); + plugins.push(new webpack.optimize.UglifyJsPlugin({ + screwIe8: true, + compress: { + warnings: false + }, + output: { + comments: false + } + })); + } + + return plugins; + }()) +}; From 436b7c213d50543c89db2bc2c9cfc4241b3be6f1 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Wed, 26 Oct 2016 11:46:34 +0000 Subject: [PATCH 64/77] [ci skip] js-precompiled 20161026-114541 --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 27ac800f5..51e255a24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1208,7 +1208,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#6be42e2bcf15db125797097df7a2dcfbf7d1e1d2" +source = "git+https://github.com/ethcore/js-precompiled.git#1c5409c9102b62600f9e5e2894221827b24721f8" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] From 5b978be03445ef0d811fdd3cf1ef4193f21f2a70 Mon Sep 17 00:00:00 2001 From: Arkadiy Paronyan Date: Wed, 26 Oct 2016 13:53:47 +0200 Subject: [PATCH 65/77] Get rid of MemoryDB denote (#2881) --- Cargo.lock | 13 ++-- Cargo.toml | 1 + ethcore/src/account_db.rs | 34 +++++----- ethcore/src/migrations/state/v7.rs | 2 +- ethcore/src/snapshot/account.rs | 12 ++-- ethcore/src/snapshot/mod.rs | 6 +- ethcore/src/snapshot/tests/helpers.rs | 3 +- ethcore/src/state/account.rs | 6 +- ethcore/src/state/mod.rs | 6 +- util/Cargo.toml | 2 +- util/rlp/Cargo.toml | 4 +- util/src/hashdb.rs | 13 ++-- util/src/journaldb/archivedb.rs | 28 +++----- util/src/journaldb/earlymergedb.rs | 29 +++----- util/src/journaldb/overlayrecentdb.rs | 56 ++++++---------- util/src/journaldb/refcounteddb.rs | 6 +- util/src/kvdb.rs | 21 +++--- util/src/lib.rs | 2 +- util/src/memorydb.rs | 78 ++++++---------------- util/src/nibbleslice.rs | 19 +++--- util/src/overlaydb.rs | 81 +++++++++++----------- util/src/trie/fatdb.rs | 10 +-- util/src/trie/fatdbmut.rs | 6 +- util/src/trie/journal.rs | 8 +-- util/src/trie/mod.rs | 12 ++-- util/src/trie/node.rs | 84 +++++++++++++++-------- util/src/trie/sectriedb.rs | 6 +- util/src/trie/sectriedbmut.rs | 6 +- util/src/trie/triedb.rs | 85 +++++++++++++----------- util/src/trie/triedbmut.rs | 96 ++++++++++++++------------- 30 files changed, 359 insertions(+), 376 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51e255a24..9df7200e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,8 +222,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "elastic-array" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" +version = "0.6.0" +source = "git+https://github.com/ethcore/elastic-array#70e4012e691b732c7c4cb04e9232799e6aa268bc" +dependencies = [ + "heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] name = "env_logger" @@ -540,7 +543,7 @@ dependencies = [ "ansi_term 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "arrayvec 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", "clippy 0.0.90 (registry+https://github.com/rust-lang/crates.io-index)", - "elastic-array 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "elastic-array 0.6.0 (git+https://github.com/ethcore/elastic-array)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "eth-secp256k1 0.5.4 (git+https://github.com/ethcore/rust-secp256k1)", "ethcore-bigint 0.1.1", @@ -1421,7 +1424,7 @@ dependencies = [ name = "rlp" version = "0.1.0" dependencies = [ - "elastic-array 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "elastic-array 0.6.0 (git+https://github.com/ethcore/elastic-array)", "ethcore-bigint 0.1.1", "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1954,7 +1957,7 @@ dependencies = [ "checksum deque 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1614659040e711785ed8ea24219140654da1729f3ec8a47a9719d041112fe7bf" "checksum docopt 0.6.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4cc0acb4ce0828c6a5a11d47baa432fe885881c27428c3a4e473e454ffe57a76" "checksum dtoa 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0dd841b58510c9618291ffa448da2e4e0f699d984d436122372f446dae62263d" -"checksum elastic-array 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4bc9250a632e7c001b741eb0ec6cee93c9a5b6d5f1879696a4b94d62b012210a" +"checksum elastic-array 0.6.0 (git+https://github.com/ethcore/elastic-array)" = "" "checksum env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "aba65b63ffcc17ffacd6cf5aa843da7c5a25e3bd4bbe0b7def8b214e411250e5" "checksum eth-secp256k1 0.5.4 (git+https://github.com/ethcore/rust-secp256k1)" = "" "checksum ethabi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f7b0c53453517f620847be51943db329276ae52f2e210cfc659e81182864be2f" diff --git a/Cargo.toml b/Cargo.toml index 0eec6ff7f..62039696c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ ipc = ["ethcore/ipc", "ethsync/ipc"] jit = ["ethcore/jit"] dev = ["clippy", "ethcore/dev", "ethcore-util/dev", "ethsync/dev", "ethcore-rpc/dev", "ethcore-dapps/dev", "ethcore-signer/dev"] json-tests = ["ethcore/json-tests"] +test-heavy = ["ethcore/test-heavy"] stratum = ["ipc"] ethkey-cli = ["ethcore/ethkey-cli"] ethstore-cli = ["ethcore/ethstore-cli"] diff --git a/ethcore/src/account_db.rs b/ethcore/src/account_db.rs index 2d00f8ed5..0761b7fba 100644 --- a/ethcore/src/account_db.rs +++ b/ethcore/src/account_db.rs @@ -96,9 +96,9 @@ impl<'db> HashDB for AccountDB<'db>{ unimplemented!() } - fn get(&self, key: &H256) -> Option<&[u8]> { + fn get(&self, key: &H256) -> Option { if key == &SHA3_NULL_RLP { - return Some(&NULL_RLP_STATIC); + return Some(DBValue::from_slice(&NULL_RLP_STATIC)); } self.db.get(&combine_key(&self.address_hash, key)) } @@ -114,7 +114,7 @@ impl<'db> HashDB for AccountDB<'db>{ unimplemented!() } - fn emplace(&mut self, _key: H256, _value: Bytes) { + fn emplace(&mut self, _key: H256, _value: DBValue) { unimplemented!() } @@ -122,7 +122,7 @@ impl<'db> HashDB for AccountDB<'db>{ unimplemented!() } - fn get_aux(&self, hash: &[u8]) -> Option> { + fn get_aux(&self, hash: &[u8]) -> Option { self.db.get_aux(hash) } } @@ -158,9 +158,9 @@ impl<'db> HashDB for AccountDBMut<'db>{ unimplemented!() } - fn get(&self, key: &H256) -> Option<&[u8]> { + fn get(&self, key: &H256) -> Option { if key == &SHA3_NULL_RLP { - return Some(&NULL_RLP_STATIC); + return Some(DBValue::from_slice(&NULL_RLP_STATIC)); } self.db.get(&combine_key(&self.address_hash, key)) } @@ -178,16 +178,16 @@ impl<'db> HashDB for AccountDBMut<'db>{ } let k = value.sha3(); let ak = combine_key(&self.address_hash, &k); - self.db.emplace(ak, value.to_vec()); + self.db.emplace(ak, DBValue::from_slice(value)); k } - fn emplace(&mut self, key: H256, value: Bytes) { + fn emplace(&mut self, key: H256, value: DBValue) { if key == SHA3_NULL_RLP { return; } let key = combine_key(&self.address_hash, &key); - self.db.emplace(key, value.to_vec()) + self.db.emplace(key, value) } fn remove(&mut self, key: &H256) { @@ -202,7 +202,7 @@ impl<'db> HashDB for AccountDBMut<'db>{ self.db.insert_aux(hash, value); } - fn get_aux(&self, hash: &[u8]) -> Option> { + fn get_aux(&self, hash: &[u8]) -> Option { self.db.get_aux(hash) } @@ -218,9 +218,9 @@ impl<'db> HashDB for Wrapping<'db> { unimplemented!() } - fn get(&self, key: &H256) -> Option<&[u8]> { + fn get(&self, key: &H256) -> Option { if key == &SHA3_NULL_RLP { - return Some(&NULL_RLP_STATIC); + return Some(DBValue::from_slice(&NULL_RLP_STATIC)); } self.0.get(key) } @@ -236,7 +236,7 @@ impl<'db> HashDB for Wrapping<'db> { unimplemented!() } - fn emplace(&mut self, _key: H256, _value: Bytes) { + fn emplace(&mut self, _key: H256, _value: DBValue) { unimplemented!() } @@ -252,9 +252,9 @@ impl<'db> HashDB for WrappingMut<'db>{ unimplemented!() } - fn get(&self, key: &H256) -> Option<&[u8]> { + fn get(&self, key: &H256) -> Option { if key == &SHA3_NULL_RLP { - return Some(&NULL_RLP_STATIC); + return Some(DBValue::from_slice(&NULL_RLP_STATIC)); } self.0.get(key) } @@ -273,7 +273,7 @@ impl<'db> HashDB for WrappingMut<'db>{ self.0.insert(value) } - fn emplace(&mut self, key: H256, value: Bytes) { + fn emplace(&mut self, key: H256, value: DBValue) { if key == SHA3_NULL_RLP { return; } @@ -286,4 +286,4 @@ impl<'db> HashDB for WrappingMut<'db>{ } self.0.remove(key) } -} \ No newline at end of file +} diff --git a/ethcore/src/migrations/state/v7.rs b/ethcore/src/migrations/state/v7.rs index 9af75a8ed..49df041eb 100644 --- a/ethcore/src/migrations/state/v7.rs +++ b/ethcore/src/migrations/state/v7.rs @@ -154,7 +154,7 @@ impl OverlayRecentV7 { // and commit the altered entries. fn migrate_journal(&self, source: Arc, mut batch: Batch, dest: &mut Database) -> Result<(), Error> { if let Some(val) = try!(source.get(None, V7_LATEST_ERA_KEY).map_err(Error::Custom)) { - try!(batch.insert(V7_LATEST_ERA_KEY.into(), val.to_owned(), dest)); + try!(batch.insert(V7_LATEST_ERA_KEY.into(), val.clone().to_vec(), dest)); let mut era = decode::(&val); loop { diff --git a/ethcore/src/snapshot/account.rs b/ethcore/src/snapshot/account.rs index 30f2cd956..7e4585365 100644 --- a/ethcore/src/snapshot/account.rs +++ b/ethcore/src/snapshot/account.rs @@ -19,7 +19,7 @@ use account_db::{AccountDB, AccountDBMut}; use snapshot::Error; -use util::{U256, FixedHash, H256, Bytes, HashDB, SHA3_EMPTY, SHA3_NULL_RLP}; +use util::{U256, FixedHash, H256, Bytes, HashDB, DBValue, SHA3_EMPTY, SHA3_NULL_RLP}; use util::trie::{TrieDB, Trie}; use rlp::{Rlp, RlpStream, Stream, UntrustedRlp, View}; @@ -112,7 +112,7 @@ impl Account { let mut stream = RlpStream::new_list(pairs.len()); for (k, v) in pairs { - stream.begin_list(2).append(&k).append(&v); + stream.begin_list(2).append(&k).append(&&*v); } let pairs_rlp = stream.out(); @@ -130,7 +130,7 @@ impl Account { match acct_db.get(&self.code_hash) { Some(c) => { used_code.insert(self.code_hash.clone()); - account_stream.append(&CodeState::Inline.raw()).append(&c); + account_stream.append(&CodeState::Inline.raw()).append(&&*c); } None => { warn!("code lookup failed during snapshot"); @@ -178,7 +178,7 @@ impl Account { CodeState::Hash => { let code_hash = try!(rlp.val_at(3)); if let Some(code) = code_map.get(&code_hash) { - acct_db.emplace(code_hash.clone(), code.clone()); + acct_db.emplace(code_hash.clone(), DBValue::from_slice(&code)); } (code_hash, None) @@ -226,7 +226,7 @@ mod tests { use snapshot::tests::helpers::fill_storage; use util::sha3::{SHA3_EMPTY, SHA3_NULL_RLP}; - use util::{Address, FixedHash, H256, HashDB}; + use util::{Address, FixedHash, H256, HashDB, DBValue}; use rlp::{UntrustedRlp, View}; use std::collections::{HashSet, HashMap}; @@ -292,7 +292,7 @@ mod tests { { let mut acct_db = AccountDBMut::new(db.as_hashdb_mut(), &addr2); - acct_db.emplace(code_hash.clone(), b"this is definitely code".to_vec()); + acct_db.emplace(code_hash.clone(), DBValue::from_slice(b"this is definitely code")); } let account1 = Account { diff --git a/ethcore/src/snapshot/mod.rs b/ethcore/src/snapshot/mod.rs index 8b032c0e1..223d769d2 100644 --- a/ethcore/src/snapshot/mod.rs +++ b/ethcore/src/snapshot/mod.rs @@ -29,7 +29,7 @@ use engines::Engine; use ids::BlockID; use views::BlockView; -use util::{Bytes, Hashable, HashDB, snappy, U256, Uint}; +use util::{Bytes, Hashable, HashDB, DBValue, snappy, U256, Uint}; use util::memorydb::MemoryDB; use util::Mutex; use util::hash::{FixedHash, H256}; @@ -369,7 +369,7 @@ pub fn chunk_state<'a>(db: &HashDB, root: &H256, writer: &Mutex x.map_or_else(U256::zero, decode), + Ok(x) => x.map_or_else(U256::zero, |v| decode(&*v)), Err(e) => panic!("Encountered potential DB corruption: {}", e), }; let value: H256 = item.into(); @@ -253,8 +253,8 @@ impl Account { self.is_cached() || match db.get(&self.code_hash) { Some(x) => { - self.code_cache = Arc::new(x.to_vec()); self.code_size = Some(x.len()); + self.code_cache = Arc::new(x.to_vec()); true }, _ => { @@ -351,7 +351,7 @@ impl Account { self.code_filth = Filth::Clean; }, (true, false) => { - db.emplace(self.code_hash.clone(), (*self.code_cache).clone()); + db.emplace(self.code_hash.clone(), DBValue::from_slice(&*self.code_cache)); self.code_size = Some(self.code_cache.len()); self.code_filth = Filth::Clean; }, diff --git a/ethcore/src/state/mod.rs b/ethcore/src/state/mod.rs index 2253ed89d..6befcad12 100644 --- a/ethcore/src/state/mod.rs +++ b/ethcore/src/state/mod.rs @@ -408,7 +408,7 @@ impl State { // account is not found in the global cache, get from the DB and insert into local let db = self.factories.trie.readonly(self.db.as_hashdb(), &self.root).expect(SEC_TRIE_DB_UNWRAP_STR); let maybe_acc = match db.get(address) { - Ok(acc) => acc.map(Account::from_rlp), + Ok(acc) => acc.map(|v| Account::from_rlp(&v)), Err(e) => panic!("Potential DB corruption encountered: {}", e), }; let r = maybe_acc.as_ref().map_or(H256::new(), |a| { @@ -648,7 +648,7 @@ impl State { // not found in the global cache, get from the DB and insert into local let db = self.factories.trie.readonly(self.db.as_hashdb(), &self.root).expect(SEC_TRIE_DB_UNWRAP_STR); let mut maybe_acc = match db.get(a) { - Ok(acc) => acc.map(Account::from_rlp), + Ok(acc) => acc.map(|v| Account::from_rlp(&v)), Err(e) => panic!("Potential DB corruption encountered: {}", e), }; if let Some(ref mut account) = maybe_acc.as_mut() { @@ -680,7 +680,7 @@ impl State { let maybe_acc = if self.db.check_account_bloom(a) { let db = self.factories.trie.readonly(self.db.as_hashdb(), &self.root).expect(SEC_TRIE_DB_UNWRAP_STR); let maybe_acc = match db.get(a) { - Ok(Some(acc)) => AccountEntry::new_clean(Some(Account::from_rlp(acc))), + Ok(Some(acc)) => AccountEntry::new_clean(Some(Account::from_rlp(&acc))), Ok(None) => AccountEntry::new_clean(None), Err(e) => panic!("Potential DB corruption encountered: {}", e), }; diff --git a/util/Cargo.toml b/util/Cargo.toml index ce2992fe5..c560a6bb5 100644 --- a/util/Cargo.toml +++ b/util/Cargo.toml @@ -18,7 +18,7 @@ rocksdb = { git = "https://github.com/ethcore/rust-rocksdb" } lazy_static = "0.2" eth-secp256k1 = { git = "https://github.com/ethcore/rust-secp256k1" } rust-crypto = "0.2.34" -elastic-array = "0.5" +elastic-array = { git = "https://github.com/ethcore/elastic-array" } rlp = { path = "rlp" } heapsize = { version = "0.3", features = ["unstable"] } itertools = "0.4" diff --git a/util/rlp/Cargo.toml b/util/rlp/Cargo.toml index c24e4cc59..7095ddb04 100644 --- a/util/rlp/Cargo.toml +++ b/util/rlp/Cargo.toml @@ -6,7 +6,7 @@ version = "0.1.0" authors = ["Ethcore "] [dependencies] -elastic-array = "0.5" +elastic-array = { git = "https://github.com/ethcore/elastic-array" } ethcore-bigint = { path = "../bigint" } lazy_static = "0.2" -rustc-serialize = "0.3" \ No newline at end of file +rustc-serialize = "0.3" diff --git a/util/src/hashdb.rs b/util/src/hashdb.rs index 55cc2a89e..395a504de 100644 --- a/util/src/hashdb.rs +++ b/util/src/hashdb.rs @@ -16,8 +16,11 @@ //! Database of byte-slices keyed to their Keccak hash. use hash::*; -use bytes::*; use std::collections::HashMap; +use elastic_array::ElasticArray256; + +/// `HashDB` value type. +pub type DBValue = ElasticArray256; /// Trait modelling datastore keyed by a 32-byte Keccak hash. pub trait HashDB: AsHashDB + Send + Sync { @@ -39,7 +42,7 @@ pub trait HashDB: AsHashDB + Send + Sync { /// assert_eq!(m.get(&hash).unwrap(), hello_bytes); /// } /// ``` - fn get(&self, key: &H256) -> Option<&[u8]>; + fn get(&self, key: &H256) -> Option; /// Check for the existance of a hash-key. /// @@ -80,7 +83,7 @@ pub trait HashDB: AsHashDB + Send + Sync { fn insert(&mut self, value: &[u8]) -> H256; /// Like `insert()` , except you provide the key and the data is all moved. - fn emplace(&mut self, key: H256, value: Bytes); + fn emplace(&mut self, key: H256, value: DBValue); /// Remove a datum previously inserted. Insertions can be "owed" such that the same number of `insert()`s may /// happen without the data being eventually being inserted into the DB. @@ -111,7 +114,7 @@ pub trait HashDB: AsHashDB + Send + Sync { } /// Get auxiliary data from hashdb. - fn get_aux(&self, _hash: &[u8]) -> Option> { + fn get_aux(&self, _hash: &[u8]) -> Option { unimplemented!(); } @@ -136,4 +139,4 @@ impl AsHashDB for T { fn as_hashdb_mut(&mut self) -> &mut HashDB { self } -} \ No newline at end of file +} diff --git a/util/src/journaldb/archivedb.rs b/util/src/journaldb/archivedb.rs index efedfb766..940f92375 100644 --- a/util/src/journaldb/archivedb.rs +++ b/util/src/journaldb/archivedb.rs @@ -65,8 +65,8 @@ impl ArchiveDB { Self::new(backing, None) } - fn payload(&self, key: &H256) -> Option { - self.backing.get(self.column, key).expect("Low-level database error. Some issue with your hard disk?").map(|v| v.to_vec()) + fn payload(&self, key: &H256) -> Option { + self.backing.get(self.column, key).expect("Low-level database error. Some issue with your hard disk?") } } @@ -85,19 +85,12 @@ impl HashDB for ArchiveDB { ret } - fn get(&self, key: &H256) -> Option<&[u8]> { + fn get(&self, key: &H256) -> Option { let k = self.overlay.raw(key); - match k { - Some((d, rc)) if rc > 0 => Some(d), - _ => { - if let Some(x) = self.payload(key) { - Some(self.overlay.denote(key, x).0) - } - else { - None - } - } + if let Some((d, rc)) = k { + if rc > 0 { return Some(d); } } + self.payload(key) } fn contains(&self, key: &H256) -> bool { @@ -108,7 +101,7 @@ impl HashDB for ArchiveDB { self.overlay.insert(value) } - fn emplace(&mut self, key: H256, value: Bytes) { + fn emplace(&mut self, key: H256, value: DBValue) { self.overlay.emplace(key, value); } @@ -120,7 +113,7 @@ impl HashDB for ArchiveDB { self.overlay.insert_aux(hash, value); } - fn get_aux(&self, hash: &[u8]) -> Option> { + fn get_aux(&self, hash: &[u8]) -> Option { if let Some(res) = self.overlay.get_aux(hash) { return Some(res) } @@ -130,7 +123,6 @@ impl HashDB for ArchiveDB { self.backing.get(self.column, &db_hash) .expect("Low-level database error. Some issue with your hard disk?") - .map(|v| v.to_vec()) } fn remove_aux(&mut self, hash: &[u8]) { @@ -396,7 +388,7 @@ mod tests { let mut jdb = new_db(&dir); // history is 1 let foo = jdb.insert(b"foo"); - jdb.emplace(bar.clone(), b"bar".to_vec()); + jdb.emplace(bar.clone(), DBValue::from_slice(b"bar")); jdb.commit_batch(0, &b"0".sha3(), None).unwrap(); foo }; @@ -497,7 +489,7 @@ mod tests { let key = jdb.insert(b"dog"); jdb.inject_batch().unwrap(); - assert_eq!(jdb.get(&key).unwrap(), b"dog"); + assert_eq!(jdb.get(&key).unwrap(), DBValue::from_slice(b"dog")); jdb.remove(&key); jdb.inject_batch().unwrap(); diff --git a/util/src/journaldb/earlymergedb.rs b/util/src/journaldb/earlymergedb.rs index 0f7c097b8..1e782c580 100644 --- a/util/src/journaldb/earlymergedb.rs +++ b/util/src/journaldb/earlymergedb.rs @@ -150,7 +150,7 @@ impl EarlyMergeDB { backing.get(col, &Self::morph_key(key, 0)).expect("Low-level database error. Some issue with your hard disk?").is_some() } - fn insert_keys(inserts: &[(H256, Bytes)], backing: &Database, col: Option, refs: &mut HashMap, batch: &mut DBTransaction, trace: bool) { + fn insert_keys(inserts: &[(H256, DBValue)], backing: &Database, col: Option, refs: &mut HashMap, batch: &mut DBTransaction, trace: bool) { for &(ref h, ref d) in inserts { if let Some(c) = refs.get_mut(h) { // already counting. increment. @@ -268,8 +268,8 @@ impl EarlyMergeDB { } } - fn payload(&self, key: &H256) -> Option { - self.backing.get(self.column, key).expect("Low-level database error. Some issue with your hard disk?").map(|v| v.to_vec()) + fn payload(&self, key: &H256) -> Option { + self.backing.get(self.column, key).expect("Low-level database error. Some issue with your hard disk?") } fn read_refs(db: &Database, col: Option) -> (Option, HashMap) { @@ -317,19 +317,12 @@ impl HashDB for EarlyMergeDB { ret } - fn get(&self, key: &H256) -> Option<&[u8]> { + fn get(&self, key: &H256) -> Option { let k = self.overlay.raw(key); - match k { - Some((d, rc)) if rc > 0 => Some(d), - _ => { - if let Some(x) = self.payload(key) { - Some(self.overlay.denote(key, x).0) - } - else { - None - } - } + if let Some((d, rc)) = k { + if rc > 0 { return Some(d) } } + self.payload(key) } fn contains(&self, key: &H256) -> bool { @@ -339,7 +332,7 @@ impl HashDB for EarlyMergeDB { fn insert(&mut self, value: &[u8]) -> H256 { self.overlay.insert(value) } - fn emplace(&mut self, key: H256, value: Bytes) { + fn emplace(&mut self, key: H256, value: DBValue) { self.overlay.emplace(key, value); } fn remove(&mut self, key: &H256) { @@ -413,7 +406,7 @@ impl JournalDB for EarlyMergeDB { .iter() .filter_map(|(k, &(_, c))| if c < 0 {Some(k.clone())} else {None}) .collect(); - let inserts: Vec<(H256, Bytes)> = drained + let inserts: Vec<(H256, _)> = drained .into_iter() .filter_map(|(k, (v, r))| if r > 0 { assert!(r == 1); Some((k, v)) } else { assert!(r >= -1); None }) .collect(); @@ -832,7 +825,7 @@ mod tests { let mut jdb = new_db(&dir); // history is 1 let foo = jdb.insert(b"foo"); - jdb.emplace(bar.clone(), b"bar".to_vec()); + jdb.emplace(bar.clone(), DBValue::from_slice(b"bar")); jdb.commit_batch(0, &b"0".sha3(), None).unwrap(); assert!(jdb.can_reconstruct_refs()); foo @@ -1088,7 +1081,7 @@ mod tests { let key = jdb.insert(b"dog"); jdb.inject_batch().unwrap(); - assert_eq!(jdb.get(&key).unwrap(), b"dog"); + assert_eq!(jdb.get(&key).unwrap(), DBValue::from_slice(b"dog")); jdb.remove(&key); jdb.inject_batch().unwrap(); diff --git a/util/src/journaldb/overlayrecentdb.rs b/util/src/journaldb/overlayrecentdb.rs index e58fe2522..83868d06b 100644 --- a/util/src/journaldb/overlayrecentdb.rs +++ b/util/src/journaldb/overlayrecentdb.rs @@ -67,7 +67,7 @@ pub struct OverlayRecentDB { #[derive(PartialEq)] struct JournalOverlay { backing_overlay: MemoryDB, // Nodes added in the history period - pending_overlay: H256FastMap, // Nodes being transfered from backing_overlay to backing db + pending_overlay: H256FastMap, // Nodes being transfered from backing_overlay to backing db journal: HashMap>, latest_era: Option, earliest_era: Option, @@ -130,7 +130,7 @@ impl OverlayRecentDB { journal_overlay.latest_era == reconstructed.latest_era } - fn payload(&self, key: &H256) -> Option { + fn payload(&self, key: &H256) -> Option { self.backing.get(self.column, key).expect("Low-level database error. Some issue with your hard disk?") } @@ -160,8 +160,8 @@ impl OverlayRecentDB { let mut inserted_keys = Vec::new(); for r in insertions.iter() { let k: H256 = r.val_at(0); - let v: Bytes = r.val_at(1); - overlay.emplace(to_short_key(&k), v); + let v = r.at(1).data(); + overlay.emplace(to_short_key(&k), DBValue::from_slice(v)); inserted_keys.push(k); count += 1; } @@ -229,7 +229,7 @@ impl JournalDB for OverlayRecentDB { let journal_overlay = self.journal_overlay.read(); let key = to_short_key(key); journal_overlay.backing_overlay.get(&key).map(|v| v.to_vec()) - .or_else(|| journal_overlay.pending_overlay.get(&key).cloned()) + .or_else(|| journal_overlay.pending_overlay.get(&key).map(|d| d.clone().to_vec())) .or_else(|| self.backing.get_by_prefix(self.column, &key[0..DB_PREFIX_LEN]).map(|b| b.to_vec())) } @@ -255,7 +255,7 @@ impl JournalDB for OverlayRecentDB { for (k, v) in insertions { r.begin_list(2); r.append(&k); - r.append(&v); + r.append(&&*v); journal_overlay.backing_overlay.emplace(to_short_key(&k), v); } r.append(&removed_keys); @@ -284,7 +284,7 @@ impl JournalDB for OverlayRecentDB { let mut ops = 0; // apply old commits' details if let Some(ref mut records) = journal_overlay.journal.get_mut(&end_era) { - let mut canon_insertions: Vec<(H256, Bytes)> = Vec::new(); + let mut canon_insertions: Vec<(H256, DBValue)> = Vec::new(); let mut canon_deletions: Vec = Vec::new(); let mut overlay_deletions: Vec = Vec::new(); let mut index = 0usize; @@ -301,7 +301,7 @@ impl JournalDB for OverlayRecentDB { for h in &journal.insertions { if let Some((d, rc)) = journal_overlay.backing_overlay.raw(&to_short_key(h)) { if rc > 0 { - canon_insertions.push((h.clone(), d.to_owned())); //TODO: optimize this to avoid data copy + canon_insertions.push((h.clone(), d)); //TODO: optimize this to avoid data copy } } } @@ -386,32 +386,18 @@ impl HashDB for OverlayRecentDB { ret } - fn get(&self, key: &H256) -> Option<&[u8]> { + fn get(&self, key: &H256) -> Option { let k = self.transaction_overlay.raw(key); - match k { - Some((d, rc)) if rc > 0 => Some(d), - _ => { - let v = { - let journal_overlay = self.journal_overlay.read(); - let key = to_short_key(key); - journal_overlay.backing_overlay.get(&key).map(|v| v.to_vec()) - .or_else(|| journal_overlay.pending_overlay.get(&key).cloned()) - }; - match v { - Some(x) => { - Some(self.transaction_overlay.denote(key, x).0) - } - _ => { - if let Some(x) = self.payload(key) { - Some(self.transaction_overlay.denote(key, x).0) - } - else { - None - } - } - } - } + if let Some((d, rc)) = k { + if rc > 0 { return Some(d) } } + let v = { + let journal_overlay = self.journal_overlay.read(); + let key = to_short_key(key); + journal_overlay.backing_overlay.get(&key) + .or_else(|| journal_overlay.pending_overlay.get(&key).cloned()) + }; + v.or_else(|| self.payload(key)) } fn contains(&self, key: &H256) -> bool { @@ -421,7 +407,7 @@ impl HashDB for OverlayRecentDB { fn insert(&mut self, value: &[u8]) -> H256 { self.transaction_overlay.insert(value) } - fn emplace(&mut self, key: H256, value: Bytes) { + fn emplace(&mut self, key: H256, value: DBValue) { self.transaction_overlay.emplace(key, value); } fn remove(&mut self, key: &H256) { @@ -692,7 +678,7 @@ mod tests { let mut jdb = new_db(&dir); // history is 1 let foo = jdb.insert(b"foo"); - jdb.emplace(bar.clone(), b"bar".to_vec()); + jdb.emplace(bar.clone(), DBValue::from_slice(b"bar")); jdb.commit_batch(0, &b"0".sha3(), None).unwrap(); assert!(jdb.can_reconstruct_refs()); foo @@ -965,7 +951,7 @@ mod tests { let key = jdb.insert(b"dog"); jdb.inject_batch().unwrap(); - assert_eq!(jdb.get(&key).unwrap(), b"dog"); + assert_eq!(jdb.get(&key).unwrap(), DBValue::from_slice(b"dog")); jdb.remove(&key); jdb.inject_batch().unwrap(); diff --git a/util/src/journaldb/refcounteddb.rs b/util/src/journaldb/refcounteddb.rs index e6a0f5dcc..57621f321 100644 --- a/util/src/journaldb/refcounteddb.rs +++ b/util/src/journaldb/refcounteddb.rs @@ -83,10 +83,10 @@ impl RefCountedDB { impl HashDB for RefCountedDB { fn keys(&self) -> HashMap { self.forward.keys() } - fn get(&self, key: &H256) -> Option<&[u8]> { self.forward.get(key) } + fn get(&self, key: &H256) -> Option { self.forward.get(key) } fn contains(&self, key: &H256) -> bool { self.forward.contains(key) } fn insert(&mut self, value: &[u8]) -> H256 { let r = self.forward.insert(value); self.inserts.push(r.clone()); r } - fn emplace(&mut self, key: H256, value: Bytes) { self.inserts.push(key.clone()); self.forward.emplace(key, value); } + fn emplace(&mut self, key: H256, value: DBValue) { self.inserts.push(key.clone()); self.forward.emplace(key, value); } fn remove(&mut self, key: &H256) { self.removes.push(key.clone()); } } @@ -326,7 +326,7 @@ mod tests { let key = jdb.insert(b"dog"); jdb.inject_batch().unwrap(); - assert_eq!(jdb.get(&key).unwrap(), b"dog"); + assert_eq!(jdb.get(&key).unwrap(), DBValue::from_slice(b"dog")); jdb.remove(&key); jdb.inject_batch().unwrap(); diff --git a/util/src/kvdb.rs b/util/src/kvdb.rs index b37af4dc9..08e856117 100644 --- a/util/src/kvdb.rs +++ b/util/src/kvdb.rs @@ -21,6 +21,7 @@ use common::*; use elastic_array::*; use std::default::Default; use std::path::PathBuf; +use hashdb::DBValue; use rlp::{UntrustedRlp, RlpType, View, Compressible}; use rocksdb::{DB, Writable, WriteBatch, WriteOptions, IteratorMode, DBIterator, Options, DBCompactionStyle, BlockBasedOptions, Direction, Cache, Column, ReadOptions}; @@ -43,12 +44,12 @@ enum DBOp { Insert { col: Option, key: ElasticArray32, - value: Bytes, + value: DBValue, }, InsertCompressed { col: Option, key: ElasticArray32, - value: Bytes, + value: DBValue, }, Delete { col: Option, @@ -71,7 +72,7 @@ impl DBTransaction { self.ops.push(DBOp::Insert { col: col, key: ekey, - value: value.to_vec(), + value: DBValue::from_slice(value), }); } @@ -82,7 +83,7 @@ impl DBTransaction { self.ops.push(DBOp::Insert { col: col, key: ekey, - value: value, + value: DBValue::from_vec(value), }); } @@ -94,7 +95,7 @@ impl DBTransaction { self.ops.push(DBOp::InsertCompressed { col: col, key: ekey, - value: value, + value: DBValue::from_vec(value), }); } @@ -110,8 +111,8 @@ impl DBTransaction { } enum KeyState { - Insert(Bytes), - InsertCompressed(Bytes), + Insert(DBValue), + InsertCompressed(DBValue), Delete, } @@ -507,7 +508,7 @@ impl Database { } /// Get value by key. - pub fn get(&self, col: Option, key: &[u8]) -> Result, String> { + pub fn get(&self, col: Option, key: &[u8]) -> Result, String> { match *self.db.read() { Some(DBAndColumns { ref db, ref cfs }) => { let overlay = &self.overlay.read()[Self::to_overlay_column(col)]; @@ -521,8 +522,8 @@ impl Database { Some(&KeyState::Delete) => Ok(None), None => { col.map_or_else( - || db.get_opt(key, &self.read_opts).map(|r| r.map(|v| v.to_vec())), - |c| db.get_cf_opt(cfs[c as usize], key, &self.read_opts).map(|r| r.map(|v| v.to_vec()))) + || db.get_opt(key, &self.read_opts).map(|r| r.map(|v| DBValue::from_slice(&v))), + |c| db.get_cf_opt(cfs[c as usize], key, &self.read_opts).map(|r| r.map(|v| DBValue::from_slice(&v)))) }, } }, diff --git a/util/src/lib.rs b/util/src/lib.rs index bebb2819a..e362459a6 100644 --- a/util/src/lib.rs +++ b/util/src/lib.rs @@ -148,7 +148,7 @@ mod timer; pub use common::*; pub use misc::*; pub use hashdb::*; -pub use memorydb::*; +pub use memorydb::MemoryDB; pub use overlaydb::*; pub use journaldb::JournalDB; pub use triehash::*; diff --git a/util/src/memorydb.rs b/util/src/memorydb.rs index d0ecb73d9..338f12b1e 100644 --- a/util/src/memorydb.rs +++ b/util/src/memorydb.rs @@ -24,8 +24,6 @@ use hashdb::*; use heapsize::*; use std::mem; use std::collections::HashMap; - -const STATIC_NULL_RLP: (&'static [u8], i32) = (&[0x80; 1], 1); use std::collections::hash_map::Entry; /// Reference-counted memory-based `HashDB` implementation. @@ -73,8 +71,8 @@ use std::collections::hash_map::Entry; /// ``` #[derive(Default, Clone, PartialEq)] pub struct MemoryDB { - data: H256FastMap<(Bytes, i32)>, - aux: HashMap, + data: H256FastMap<(DBValue, i32)>, + aux: HashMap, } impl MemoryDB { @@ -116,12 +114,12 @@ impl MemoryDB { } /// Return the internal map of hashes to data, clearing the current state. - pub fn drain(&mut self) -> H256FastMap<(Bytes, i32)> { + pub fn drain(&mut self) -> H256FastMap<(DBValue, i32)> { mem::replace(&mut self.data, H256FastMap::default()) } /// Return the internal map of auxiliary data, clearing the current state. - pub fn drain_aux(&mut self) -> HashMap { + pub fn drain_aux(&mut self) -> HashMap { mem::replace(&mut self.aux, HashMap::new()) } @@ -130,25 +128,11 @@ impl MemoryDB { /// /// Even when Some is returned, the data is only guaranteed to be useful /// when the refs > 0. - pub fn raw(&self, key: &H256) -> Option<(&[u8], i32)> { + pub fn raw(&self, key: &H256) -> Option<(DBValue, i32)> { if key == &SHA3_NULL_RLP { - return Some(STATIC_NULL_RLP.clone()); + return Some((DBValue::from_slice(&NULL_RLP_STATIC), 1)); } - self.data.get(key).map(|&(ref val, rc)| (&val[..], rc)) - } - - /// Denote than an existing value has the given key. Used when a key gets removed without - /// a prior insert and thus has a negative reference with no value. - /// - /// May safely be called even if the key's value is known, in which case it will be a no-op. - pub fn denote(&self, key: &H256, value: Bytes) -> (&[u8], i32) { - if self.raw(key) == None { - unsafe { - let p = &self.data as *const H256FastMap<(Bytes, i32)> as *mut H256FastMap<(Bytes, i32)>; - (*p).insert(key.clone(), (value, 0)); - } - } - self.raw(key).expect("entry just inserted into data; qed") + self.data.get(key).cloned() } /// Returns the size of allocated heap memory @@ -170,7 +154,7 @@ impl MemoryDB { entry.get_mut().1 -= 1; }, Entry::Vacant(entry) => { - entry.insert((Bytes::new(), -1)); + entry.insert((DBValue::new(), -1)); } } } @@ -197,13 +181,13 @@ impl MemoryDB { static NULL_RLP_STATIC: [u8; 1] = [0x80; 1]; impl HashDB for MemoryDB { - fn get(&self, key: &H256) -> Option<&[u8]> { + fn get(&self, key: &H256) -> Option { if key == &SHA3_NULL_RLP { - return Some(&NULL_RLP_STATIC); + return Some(DBValue::from_slice(&NULL_RLP_STATIC)); } match self.data.get(key) { - Some(&(ref d, rc)) if rc > 0 => Some(d), + Some(&(ref d, rc)) if rc > 0 => Some(d.clone()), _ => None } } @@ -230,20 +214,20 @@ impl HashDB for MemoryDB { let key = value.sha3(); if match self.data.get_mut(&key) { Some(&mut (ref mut old_value, ref mut rc @ -0x80000000i32 ... 0)) => { - *old_value = value.into(); + *old_value = DBValue::from_slice(value); *rc += 1; false }, Some(&mut (_, ref mut x)) => { *x += 1; false } , None => true, }{ // ... None falls through into... - self.data.insert(key.clone(), (value.into(), 1)); + self.data.insert(key.clone(), (DBValue::from_slice(value), 1)); } key } - fn emplace(&mut self, key: H256, value: Bytes) { - if value == &NULL_RLP { + fn emplace(&mut self, key: H256, value: DBValue) { + if &*value == &NULL_RLP { return; } @@ -269,15 +253,15 @@ impl HashDB for MemoryDB { Some(&mut (_, ref mut x)) => { *x -= 1; false } None => true }{ // ... None falls through into... - self.data.insert(key.clone(), (Bytes::new(), -1)); + self.data.insert(key.clone(), (DBValue::new(), -1)); } } fn insert_aux(&mut self, hash: Vec, value: Vec) { - self.aux.insert(hash, value); + self.aux.insert(hash, DBValue::from_vec(value)); } - fn get_aux(&self, hash: &[u8]) -> Option> { + fn get_aux(&self, hash: &[u8]) -> Option { self.aux.get(hash).cloned() } @@ -286,24 +270,6 @@ impl HashDB for MemoryDB { } } -#[test] -fn memorydb_denote() { - let mut m = MemoryDB::new(); - let hello_bytes = b"Hello world!"; - let hash = m.insert(hello_bytes); - assert_eq!(m.get(&hash).unwrap(), b"Hello world!"); - - for _ in 0..1000 { - let r = H256::random(); - let k = r.sha3(); - let (v, rc) = m.denote(&k, r.to_vec()); - assert_eq!(v, &*r); - assert_eq!(rc, 0); - } - - assert_eq!(m.get(&hash).unwrap(), b"Hello world!"); -} - #[test] fn memorydb_remove_and_purge() { let hello_bytes = b"Hello world!"; @@ -337,12 +303,12 @@ fn consolidate() { main.remove(&remove_key); let insert_key = other.insert(b"arf"); - main.emplace(insert_key, b"arf".to_vec()); + main.emplace(insert_key, DBValue::from_slice(b"arf")); main.consolidate(other); let overlay = main.drain(); - assert_eq!(overlay.get(&remove_key).unwrap(), &(b"doggo".to_vec(), 0)); - assert_eq!(overlay.get(&insert_key).unwrap(), &(b"arf".to_vec(), 2)); -} \ No newline at end of file + assert_eq!(overlay.get(&remove_key).unwrap(), &(DBValue::from_slice(b"doggo"), 0)); + assert_eq!(overlay.get(&insert_key).unwrap(), &(DBValue::from_slice(b"arf"), 2)); +} diff --git a/util/src/nibbleslice.rs b/util/src/nibbleslice.rs index e10def40a..7daec55ca 100644 --- a/util/src/nibbleslice.rs +++ b/util/src/nibbleslice.rs @@ -17,7 +17,7 @@ //! Nibble-orientated view onto byte-slice, allowing nibble-precision offsets. use std::cmp::*; use std::fmt; -use bytes::*; +use elastic_array::ElasticArray36; /// Nibble-orientated view onto byte-slice, allowing nibble-precision offsets. /// @@ -149,9 +149,9 @@ impl<'a, 'view> NibbleSlice<'a> where 'a: 'view { } /// Encode while nibble slice in prefixed hex notation, noting whether it `is_leaf`. - pub fn encoded(&self, is_leaf: bool) -> Bytes { + pub fn encoded(&self, is_leaf: bool) -> ElasticArray36 { let l = self.len(); - let mut r = Bytes::with_capacity(l / 2 + 1); + let mut r = ElasticArray36::new(); let mut i = l % 2; r.push(if i == 1 {0x10 + self.at(0)} else {0} + if is_leaf {0x20} else {0}); while i < l { @@ -163,9 +163,9 @@ impl<'a, 'view> NibbleSlice<'a> where 'a: 'view { /// Encode only the leftmost `n` bytes of the nibble slice in prefixed hex notation, /// noting whether it `is_leaf`. - pub fn encoded_leftmost(&self, n: usize, is_leaf: bool) -> Bytes { + pub fn encoded_leftmost(&self, n: usize, is_leaf: bool) -> ElasticArray36 { let l = min(self.len(), n); - let mut r = Bytes::with_capacity(l / 2 + 1); + let mut r = ElasticArray36::new(); let mut i = l % 2; r.push(if i == 1 {0x10 + self.at(0)} else {0} + if is_leaf {0x20} else {0}); while i < l { @@ -212,6 +212,7 @@ impl<'a> fmt::Debug for NibbleSlice<'a> { #[cfg(test)] mod tests { use super::NibbleSlice; + use elastic_array::ElasticArray36; static D: &'static [u8;3] = &[0x01u8, 0x23, 0x45]; #[test] @@ -254,10 +255,10 @@ mod tests { #[test] fn encoded() { let n = NibbleSlice::new(D); - assert_eq!(n.encoded(false), &[0x00, 0x01, 0x23, 0x45]); - assert_eq!(n.encoded(true), &[0x20, 0x01, 0x23, 0x45]); - assert_eq!(n.mid(1).encoded(false), &[0x11, 0x23, 0x45]); - assert_eq!(n.mid(1).encoded(true), &[0x31, 0x23, 0x45]); + assert_eq!(n.encoded(false), ElasticArray36::from_slice(&[0x00, 0x01, 0x23, 0x45])); + assert_eq!(n.encoded(true), ElasticArray36::from_slice(&[0x20, 0x01, 0x23, 0x45])); + assert_eq!(n.mid(1).encoded(false), ElasticArray36::from_slice(&[0x11, 0x23, 0x45])); + assert_eq!(n.mid(1).encoded(true), ElasticArray36::from_slice(&[0x31, 0x23, 0x45])); } #[test] diff --git a/util/src/overlaydb.rs b/util/src/overlaydb.rs index 4a11961b6..9ebc7d1d4 100644 --- a/util/src/overlaydb.rs +++ b/util/src/overlaydb.rs @@ -18,7 +18,6 @@ use error::*; use hash::*; -use bytes::*; use rlp::*; use hashdb::*; use memorydb::*; @@ -101,21 +100,21 @@ impl OverlayDB { pub fn commit_refs(&self, key: &H256) -> i32 { self.overlay.raw(key).map_or(0, |(_, refs)| refs) } /// Get the refs and value of the given key. - fn payload(&self, key: &H256) -> Option<(Bytes, u32)> { + fn payload(&self, key: &H256) -> Option<(DBValue, u32)> { self.backing.get(self.column, key) .expect("Low-level database error. Some issue with your hard disk?") .map(|d| { let r = Rlp::new(&d); - (r.at(1).as_val(), r.at(0).as_val()) + (DBValue::from_slice(r.at(1).data()), r.at(0).as_val()) }) } /// Put the refs and value of the given key, possibly deleting it from the db. - fn put_payload_in_batch(&self, batch: &mut DBTransaction, key: &H256, payload: (Bytes, u32)) -> bool { + fn put_payload_in_batch(&self, batch: &mut DBTransaction, key: &H256, payload: (DBValue, u32)) -> bool { if payload.1 > 0 { let mut s = RlpStream::new_list(2); s.append(&payload.1); - s.append(&payload.0); + s.append(&&*payload.0); batch.put(self.column, key, s.as_raw()); false } else { @@ -140,29 +139,31 @@ impl HashDB for OverlayDB { } ret } - fn get(&self, key: &H256) -> Option<&[u8]> { + fn get(&self, key: &H256) -> Option { // return ok if positive; if negative, check backing - might be enough references there to make // it positive again. let k = self.overlay.raw(key); - match k { - Some((d, rc)) if rc > 0 => Some(d), - _ => { - let memrc = k.map_or(0, |(_, rc)| rc); - match self.payload(key) { - Some(x) => { - let (d, rc) = x; - if rc as i32 + memrc > 0 { - Some(self.overlay.denote(key, d).0) - } - else { - None - } - } - // Replace above match arm with this once https://github.com/rust-lang/rust/issues/15287 is done. - //Some((d, rc)) if rc + memrc > 0 => Some(d), - _ => None, + let memrc = { + if let Some((d, rc)) = k { + if rc > 0 { return Some(d); } + rc + } else { + 0 + } + }; + match self.payload(key) { + Some(x) => { + let (d, rc) = x; + if rc as i32 + memrc > 0 { + Some(d) + } + else { + None } } + // Replace above match arm with this once https://github.com/rust-lang/rust/issues/15287 is done. + //Some((d, rc)) if rc + memrc > 0 => Some(d), + _ => None, } } fn contains(&self, key: &H256) -> bool { @@ -186,7 +187,7 @@ impl HashDB for OverlayDB { } } fn insert(&mut self, value: &[u8]) -> H256 { self.overlay.insert(value) } - fn emplace(&mut self, key: H256, value: Bytes) { self.overlay.emplace(key, value); } + fn emplace(&mut self, key: H256, value: DBValue) { self.overlay.emplace(key, value); } fn remove(&mut self, key: &H256) { self.overlay.remove(key); } } @@ -211,7 +212,7 @@ fn overlaydb_revert() { fn overlaydb_overlay_insert_and_remove() { let mut trie = OverlayDB::new_temp(); let h = trie.insert(b"hello world"); - assert_eq!(trie.get(&h).unwrap(), b"hello world"); + assert_eq!(trie.get(&h).unwrap(), DBValue::from_slice(b"hello world")); trie.remove(&h); assert_eq!(trie.get(&h), None); } @@ -220,11 +221,11 @@ fn overlaydb_overlay_insert_and_remove() { fn overlaydb_backing_insert_revert() { let mut trie = OverlayDB::new_temp(); let h = trie.insert(b"hello world"); - assert_eq!(trie.get(&h).unwrap(), b"hello world"); + assert_eq!(trie.get(&h).unwrap(), DBValue::from_slice(b"hello world")); trie.commit().unwrap(); - assert_eq!(trie.get(&h).unwrap(), b"hello world"); + assert_eq!(trie.get(&h).unwrap(), DBValue::from_slice(b"hello world")); trie.revert(); - assert_eq!(trie.get(&h).unwrap(), b"hello world"); + assert_eq!(trie.get(&h).unwrap(), DBValue::from_slice(b"hello world")); } #[test] @@ -248,7 +249,7 @@ fn overlaydb_backing_remove_revert() { trie.remove(&h); assert_eq!(trie.get(&h), None); trie.revert(); - assert_eq!(trie.get(&h).unwrap(), b"hello world"); + assert_eq!(trie.get(&h).unwrap(), DBValue::from_slice(b"hello world")); } #[test] @@ -266,29 +267,29 @@ fn overlaydb_negative() { fn overlaydb_complex() { let mut trie = OverlayDB::new_temp(); let hfoo = trie.insert(b"foo"); - assert_eq!(trie.get(&hfoo).unwrap(), b"foo"); + assert_eq!(trie.get(&hfoo).unwrap(), DBValue::from_slice(b"foo")); let hbar = trie.insert(b"bar"); - assert_eq!(trie.get(&hbar).unwrap(), b"bar"); + assert_eq!(trie.get(&hbar).unwrap(), DBValue::from_slice(b"bar")); trie.commit().unwrap(); - assert_eq!(trie.get(&hfoo).unwrap(), b"foo"); - assert_eq!(trie.get(&hbar).unwrap(), b"bar"); + assert_eq!(trie.get(&hfoo).unwrap(), DBValue::from_slice(b"foo")); + assert_eq!(trie.get(&hbar).unwrap(), DBValue::from_slice(b"bar")); trie.insert(b"foo"); // two refs - assert_eq!(trie.get(&hfoo).unwrap(), b"foo"); + assert_eq!(trie.get(&hfoo).unwrap(), DBValue::from_slice(b"foo")); trie.commit().unwrap(); - assert_eq!(trie.get(&hfoo).unwrap(), b"foo"); - assert_eq!(trie.get(&hbar).unwrap(), b"bar"); + assert_eq!(trie.get(&hfoo).unwrap(), DBValue::from_slice(b"foo")); + assert_eq!(trie.get(&hbar).unwrap(), DBValue::from_slice(b"bar")); trie.remove(&hbar); // zero refs - delete assert_eq!(trie.get(&hbar), None); trie.remove(&hfoo); // one ref - keep - assert_eq!(trie.get(&hfoo).unwrap(), b"foo"); + assert_eq!(trie.get(&hfoo).unwrap(), DBValue::from_slice(b"foo")); trie.commit().unwrap(); - assert_eq!(trie.get(&hfoo).unwrap(), b"foo"); + assert_eq!(trie.get(&hfoo).unwrap(), DBValue::from_slice(b"foo")); trie.remove(&hfoo); // zero ref - would delete, but... assert_eq!(trie.get(&hfoo), None); trie.insert(b"foo"); // one ref - keep after all. - assert_eq!(trie.get(&hfoo).unwrap(), b"foo"); + assert_eq!(trie.get(&hfoo).unwrap(), DBValue::from_slice(b"foo")); trie.commit().unwrap(); - assert_eq!(trie.get(&hfoo).unwrap(), b"foo"); + assert_eq!(trie.get(&hfoo).unwrap(), DBValue::from_slice(b"foo")); trie.remove(&hfoo); // zero ref - delete assert_eq!(trie.get(&hfoo), None); trie.commit().unwrap(); // diff --git a/util/src/trie/fatdb.rs b/util/src/trie/fatdb.rs index f4c65a84b..700156429 100644 --- a/util/src/trie/fatdb.rs +++ b/util/src/trie/fatdb.rs @@ -16,7 +16,7 @@ use hash::H256; use sha3::Hashable; -use hashdb::HashDB; +use hashdb::{HashDB, DBValue}; use super::{TrieDB, Trie, TrieDBIterator, TrieItem, Recorder}; /// A `Trie` implementation which hashes keys and uses a generic `HashDB` backing database. @@ -58,7 +58,7 @@ impl<'db> Trie for FatDB<'db> { self.raw.contains(&key.sha3()) } - fn get_recorded<'a, 'b, R: 'b>(&'a self, key: &'b [u8], rec: &'b mut R) -> super::Result> + fn get_recorded<'a, 'b, R: 'b>(&'a self, key: &'b [u8], rec: &'b mut R) -> super::Result> where 'a: 'b, R: Recorder { self.raw.get_recorded(&key.sha3(), rec) @@ -88,7 +88,7 @@ impl<'db> Iterator for FatDBIterator<'db> { self.trie_iterator.next() .map(|res| res.map(|(hash, value)| { - (self.trie.db().get_aux(&hash).expect("Missing fatdb hash"), value) + (self.trie.db().get_aux(&hash).expect("Missing fatdb hash").to_vec(), value) }) ) } @@ -106,6 +106,6 @@ fn fatdb_to_trie() { t.insert(&[0x01u8, 0x23], &[0x01u8, 0x23]).unwrap(); } let t = FatDB::new(&memdb, &root).unwrap(); - assert_eq!(t.get(&[0x01u8, 0x23]).unwrap().unwrap(), &[0x01u8, 0x23]); - assert_eq!(t.iter().unwrap().map(Result::unwrap).collect::>(), vec![(vec![0x01u8, 0x23], &[0x01u8, 0x23] as &[u8])]); + assert_eq!(t.get(&[0x01u8, 0x23]).unwrap().unwrap(), DBValue::from_slice(&[0x01u8, 0x23])); + assert_eq!(t.iter().unwrap().map(Result::unwrap).collect::>(), vec![(vec![0x01u8, 0x23], DBValue::from_slice(&[0x01u8, 0x23] as &[u8]))]); } diff --git a/util/src/trie/fatdbmut.rs b/util/src/trie/fatdbmut.rs index 3298541b7..fa1c168e8 100644 --- a/util/src/trie/fatdbmut.rs +++ b/util/src/trie/fatdbmut.rs @@ -16,7 +16,7 @@ use hash::H256; use sha3::Hashable; -use hashdb::HashDB; +use hashdb::{HashDB, DBValue}; use super::{TrieDBMut, TrieMut}; /// A mutable `Trie` implementation which hashes keys and uses a generic `HashDB` backing database. @@ -66,7 +66,7 @@ impl<'db> TrieMut for FatDBMut<'db> { self.raw.contains(&key.sha3()) } - fn get<'a, 'key>(&'a self, key: &'key [u8]) -> super::Result> + fn get<'a, 'key>(&'a self, key: &'key [u8]) -> super::Result> where 'a: 'key { self.raw.get(&key.sha3()) @@ -98,5 +98,5 @@ fn fatdb_to_trie() { t.insert(&[0x01u8, 0x23], &[0x01u8, 0x23]).unwrap(); } let t = TrieDB::new(&memdb, &root).unwrap(); - assert_eq!(t.get(&(&[0x01u8, 0x23]).sha3()).unwrap().unwrap(), &[0x01u8, 0x23]); + assert_eq!(t.get(&(&[0x01u8, 0x23]).sha3()).unwrap().unwrap(), DBValue::from_slice(&[0x01u8, 0x23])); } diff --git a/util/src/trie/journal.rs b/util/src/trie/journal.rs index 4ffd7cf5c..49bd1bf0f 100644 --- a/util/src/trie/journal.rs +++ b/util/src/trie/journal.rs @@ -24,7 +24,7 @@ use hashdb::*; /// Type of operation for the backing database - either a new node or a node deletion. #[derive(Debug)] enum Operation { - New(H256, Bytes), + New(H256, DBValue), Delete(H256), } @@ -52,16 +52,16 @@ impl Journal { /// Given the RLP that encodes a node, append a reference to that node `out` and leave `journal` /// such that the reference is valid, once applied. - pub fn new_node(&mut self, rlp: Bytes, out: &mut RlpStream) { + pub fn new_node(&mut self, rlp: DBValue, out: &mut RlpStream) { if rlp.len() >= 32 { let rlp_sha3 = rlp.sha3(); - trace!("new_node: reference node {:?} => {:?}", rlp_sha3, rlp.pretty()); + trace!("new_node: reference node {:?} => {:?}", rlp_sha3, &*rlp); out.append(&rlp_sha3); self.0.push(Operation::New(rlp_sha3, rlp)); } else { - trace!("new_node: inline node {:?}", rlp.pretty()); + trace!("new_node: inline node {:?}", &*rlp); out.append_raw(&rlp, 1); } } diff --git a/util/src/trie/mod.rs b/util/src/trie/mod.rs index 952eb8894..d4cc04962 100644 --- a/util/src/trie/mod.rs +++ b/util/src/trie/mod.rs @@ -18,7 +18,7 @@ use std::fmt; use hash::H256; -use hashdb::HashDB; +use hashdb::{HashDB, DBValue}; /// Export the standardmap module. pub mod standardmap; @@ -76,7 +76,7 @@ impl fmt::Display for TrieError { pub type Result = ::std::result::Result>; /// Trie-Item type. -pub type TrieItem<'a> = Result<(Vec, &'a [u8])>; +pub type TrieItem<'a> = Result<(Vec, DBValue)>; /// A key-value datastore implemented as a database-backed modified Merkle tree. pub trait Trie { @@ -92,13 +92,13 @@ pub trait Trie { } /// What is the value of the given key in this trie? - fn get<'a, 'key>(&'a self, key: &'key [u8]) -> Result> where 'a: 'key { + fn get<'a, 'key>(&'a self, key: &'key [u8]) -> Result> where 'a: 'key { self.get_recorded(key, &mut recorder::NoOp) } /// 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. - fn get_recorded<'a, 'b, R: 'b>(&'a self, key: &'b [u8], rec: &'b mut R) -> Result> + fn get_recorded<'a, 'b, R: 'b>(&'a self, key: &'b [u8], rec: &'b mut R) -> Result> where 'a: 'b, R: Recorder; /// Returns an iterator over elements of trie. @@ -119,7 +119,7 @@ pub trait TrieMut { } /// What is the value of the given key in this trie? - fn get<'a, 'key>(&'a self, key: &'key [u8]) -> Result> where 'a: 'key; + fn get<'a, 'key>(&'a self, key: &'key [u8]) -> Result> where 'a: 'key; /// Insert a `key`/`value` pair into the trie. An `empty` value is equivalent to removing /// `key` from the trie. @@ -188,7 +188,7 @@ impl<'db> Trie for TrieKinds<'db> { wrapper!(self, contains, key) } - fn get_recorded<'a, 'b, R: 'b>(&'a self, key: &'b [u8], r: &'b mut R) -> Result> + fn get_recorded<'a, 'b, R: 'b>(&'a self, key: &'b [u8], r: &'b mut R) -> Result> where 'a: 'b, R: Recorder { wrapper!(self, get_recorded, key, r) } diff --git a/util/src/trie/node.rs b/util/src/trie/node.rs index 8e1c55c73..e1f71fdc0 100644 --- a/util/src/trie/node.rs +++ b/util/src/trie/node.rs @@ -14,27 +14,51 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +use elastic_array::ElasticArray36; use nibbleslice::*; use bytes::*; use rlp::*; use super::journal::*; +use hashdb::DBValue; + +/// Partial node key type. +pub type NodeKey = ElasticArray36; /// Type of node in the trie and essential information thereof. -#[derive(Clone, Eq, PartialEq, Debug)] -pub enum Node<'a> { +#[derive(Eq, PartialEq, Debug)] +pub enum Node { /// Null trie node; could be an empty root or an empty branch entry. Empty, /// Leaf node; has key slice and value. Value may not be empty. - Leaf(NibbleSlice<'a>, &'a [u8]), + Leaf(NodeKey, DBValue), /// Extension node; has key slice and node data. Data may not be null. - Extension(NibbleSlice<'a>, &'a [u8]), + Extension(NodeKey, DBValue), /// Branch node; has array of 16 child nodes (each possibly null) and an optional immediate node data. - Branch([&'a [u8]; 16], Option<&'a [u8]>) + Branch([NodeKey; 16], Option) } -impl<'a> Node<'a> { +impl Clone for Node { + fn clone(&self) -> Node { + match *self { + Node::Empty => Node::Empty, + Node::Leaf(ref k, ref v) => Node::Leaf(k.clone(), v.clone()), + Node::Extension(ref k, ref v) => Node::Extension(k.clone(), v.clone()), + Node::Branch(ref k, ref v) => { + let mut branch = [NodeKey::new(), NodeKey::new(), NodeKey::new(), NodeKey::new(), NodeKey::new(), + NodeKey::new(), NodeKey::new(), NodeKey::new(), NodeKey::new(), NodeKey::new(), NodeKey::new(), + NodeKey::new(), NodeKey::new(), NodeKey::new(), NodeKey::new(), NodeKey::new()]; + for i in 0 .. 16 { + branch[i] = k[i].clone(); + } + Node::Branch(branch, v.clone()) + } + } + } +} + +impl Node { /// Decode the `node_rlp` and return the Node. - pub fn decoded(node_rlp: &'a [u8]) -> Node<'a> { + pub fn decoded(node_rlp: &[u8]) -> Node { let r = Rlp::new(node_rlp); match r.prototype() { // either leaf or extension - decode first item with NibbleSlice::??? @@ -43,16 +67,18 @@ impl<'a> Node<'a> { // if extension, second item is a node (either SHA3 to be looked up and // fed back into this function or inline RLP which can be fed back into this function). Prototype::List(2) => match NibbleSlice::from_encoded(r.at(0).data()) { - (slice, true) => Node::Leaf(slice, r.at(1).data()), - (slice, false) => Node::Extension(slice, r.at(1).as_raw()), + (slice, true) => Node::Leaf(slice.encoded(true), DBValue::from_slice(r.at(1).data())), + (slice, false) => Node::Extension(slice.encoded(false), DBValue::from_slice(r.at(1).as_raw())), }, // branch - first 16 are nodes, 17th is a value (or empty). Prototype::List(17) => { - let mut nodes: [&'a [u8]; 16] = [&[], &[], &[], &[], &[], &[], &[], &[], &[], &[], &[], &[], &[], &[], &[], &[]]; + let mut nodes: [NodeKey; 16] = [NodeKey::new(), NodeKey::new(), NodeKey::new(), NodeKey::new(), NodeKey::new(), + NodeKey::new(), NodeKey::new(), NodeKey::new(), NodeKey::new(), NodeKey::new(), NodeKey::new(), + NodeKey::new(), NodeKey::new(), NodeKey::new(), NodeKey::new(), NodeKey::new()]; for i in 0..16 { - nodes[i] = r.at(i).as_raw(); + nodes[i] = NodeKey::from_slice(r.at(i).as_raw()); } - Node::Branch(nodes, if r.at(16).is_empty() { None } else { Some(r.at(16).data()) }) + Node::Branch(nodes, if r.at(16).is_empty() { None } else { Some(DBValue::from_slice(r.at(16).data())) }) }, // an empty branch index. Prototype::Data(0) => Node::Empty, @@ -69,23 +95,23 @@ impl<'a> Node<'a> { match *self { Node::Leaf(ref slice, ref value) => { let mut stream = RlpStream::new_list(2); - stream.append(&slice.encoded(true)); - stream.append(value); + stream.append(&&**slice); + stream.append(&&**value); stream.out() }, - Node::Extension(ref slice, raw_rlp) => { + Node::Extension(ref slice, ref raw_rlp) => { let mut stream = RlpStream::new_list(2); - stream.append(&slice.encoded(false)); - stream.append_raw(raw_rlp, 1); + stream.append(&&**slice); + stream.append_raw(&&*raw_rlp, 1); stream.out() }, Node::Branch(ref nodes, ref value) => { let mut stream = RlpStream::new_list(17); for i in 0..16 { - stream.append_raw(nodes[i], 1); + stream.append_raw(&*nodes[i], 1); } match *value { - Some(n) => { stream.append(&n); }, + Some(ref n) => { stream.append(&&**n); }, None => { stream.append_empty_data(); }, } stream.out() @@ -100,26 +126,26 @@ impl<'a> Node<'a> { /// Encode the node, adding it to `journal` if necessary and return the RLP valid for /// insertion into a parent node. - pub fn encoded_and_added(&self, journal: &mut Journal) -> Bytes { + pub fn encoded_and_added(&self, journal: &mut Journal) -> DBValue { let mut stream = RlpStream::new(); match *self { Node::Leaf(ref slice, ref value) => { stream.begin_list(2); - stream.append(&slice.encoded(true)); - stream.append(value); + stream.append(&&**slice); + stream.append(&&**value); }, - Node::Extension(ref slice, raw_rlp) => { + Node::Extension(ref slice, ref raw_rlp) => { stream.begin_list(2); - stream.append(&slice.encoded(false)); - stream.append_raw(raw_rlp, 1); + stream.append(&&**slice); + stream.append_raw(&&**raw_rlp, 1); }, Node::Branch(ref nodes, ref value) => { stream.begin_list(17); for i in 0..16 { - stream.append_raw(nodes[i], 1); + stream.append_raw(&*nodes[i], 1); } match *value { - Some(n) => { stream.append(&n); }, + Some(ref n) => { stream.append(&&**n); }, None => { stream.append_empty_data(); }, } }, @@ -127,13 +153,13 @@ impl<'a> Node<'a> { stream.append_empty_data(); } } - let node = stream.out(); + let node = DBValue::from_slice(stream.as_raw()); match node.len() { 0 ... 31 => node, _ => { let mut stream = RlpStream::new(); journal.new_node(node, &mut stream); - stream.out() + DBValue::from_slice(stream.as_raw()) } } } diff --git a/util/src/trie/sectriedb.rs b/util/src/trie/sectriedb.rs index d7108dc3e..b1d7bbc0c 100644 --- a/util/src/trie/sectriedb.rs +++ b/util/src/trie/sectriedb.rs @@ -16,7 +16,7 @@ use hash::H256; use sha3::Hashable; -use hashdb::HashDB; +use hashdb::{HashDB, DBValue}; use super::triedb::TrieDB; use super::{Trie, TrieItem, Recorder}; @@ -59,7 +59,7 @@ impl<'db> Trie for SecTrieDB<'db> { self.raw.contains(&key.sha3()) } - fn get_recorded<'a, 'b, R: 'b>(&'a self, key: &'b [u8], rec: &'b mut R) -> super::Result> + fn get_recorded<'a, 'b, R: 'b>(&'a self, key: &'b [u8], rec: &'b mut R) -> super::Result> where 'a: 'b, R: Recorder { self.raw.get_recorded(&key.sha3(), rec) @@ -79,5 +79,5 @@ fn trie_to_sectrie() { t.insert(&(&[0x01u8, 0x23]).sha3(), &[0x01u8, 0x23]).unwrap(); } let t = SecTrieDB::new(&memdb, &root).unwrap(); - assert_eq!(t.get(&[0x01u8, 0x23]).unwrap().unwrap(), &[0x01u8, 0x23]); + assert_eq!(t.get(&[0x01u8, 0x23]).unwrap().unwrap(), DBValue::from_slice(&[0x01u8, 0x23])); } diff --git a/util/src/trie/sectriedbmut.rs b/util/src/trie/sectriedbmut.rs index d6980d48b..16cc376fe 100644 --- a/util/src/trie/sectriedbmut.rs +++ b/util/src/trie/sectriedbmut.rs @@ -16,7 +16,7 @@ use hash::H256; use sha3::Hashable; -use hashdb::HashDB; +use hashdb::{HashDB, DBValue}; use super::triedbmut::TrieDBMut; use super::TrieMut; @@ -62,7 +62,7 @@ impl<'db> TrieMut for SecTrieDBMut<'db> { self.raw.contains(&key.sha3()) } - fn get<'a, 'key>(&'a self, key: &'key [u8]) -> super::Result> + fn get<'a, 'key>(&'a self, key: &'key [u8]) -> super::Result> where 'a: 'key { self.raw.get(&key.sha3()) @@ -90,5 +90,5 @@ fn sectrie_to_trie() { t.insert(&[0x01u8, 0x23], &[0x01u8, 0x23]).unwrap(); } let t = TrieDB::new(&memdb, &root).unwrap(); - assert_eq!(t.get(&(&[0x01u8, 0x23]).sha3()).unwrap().unwrap(), &[0x01u8, 0x23]); + assert_eq!(t.get(&(&[0x01u8, 0x23]).sha3()).unwrap().unwrap(), DBValue::from_slice(&[0x01u8, 0x23])); } diff --git a/util/src/trie/triedb.rs b/util/src/trie/triedb.rs index 72604892d..ad1e509a0 100644 --- a/util/src/trie/triedb.rs +++ b/util/src/trie/triedb.rs @@ -44,7 +44,7 @@ use super::{Trie, TrieItem, TrieError}; /// TrieDBMut::new(&mut memdb, &mut root).insert(b"foo", b"bar").unwrap(); /// let t = TrieDB::new(&memdb, &root).unwrap(); /// assert!(t.contains(b"foo").unwrap()); -/// assert_eq!(t.get(b"foo").unwrap().unwrap(), b"bar"); +/// assert_eq!(t.get(b"foo").unwrap().unwrap(), DBValue::from_slice(b"bar")); /// assert!(t.db_items_remaining().unwrap().is_empty()); /// } /// ``` @@ -119,8 +119,8 @@ impl<'db> TrieDB<'db> { }; match node { - Node::Extension(_, payload) => try!(handle_payload(payload)), - Node::Branch(payloads, _) => for payload in &payloads { try!(handle_payload(payload)) }, + Node::Extension(_, ref payload) => try!(handle_payload(payload)), + Node::Branch(ref payloads, _) => for payload in payloads { try!(handle_payload(payload)) }, _ => {}, } @@ -129,18 +129,18 @@ impl<'db> TrieDB<'db> { /// Get the root node's RLP. fn root_node(&self, r: &mut R) -> super::Result { - self.root_data(r).map(Node::decoded) + self.root_data(r).map(|d| Node::decoded(&d)) } /// Get the data of the root node. - fn root_data<'a, R: 'a + Recorder>(&self, r: &'a mut R) -> super::Result<&[u8]> { + fn root_data<'a, R: 'a + Recorder>(&self, r: &'a mut R) -> super::Result { self.db.get(self.root).ok_or_else(|| Box::new(TrieError::InvalidStateRoot(*self.root))) - .map(|node| { r.record(self.root, node, 0); node }) + .map(|node| { r.record(self.root, &*node, 0); node }) } /// Get the root node as a `Node`. fn get_node<'a, R: 'a + Recorder>(&'db self, node: &'db [u8], r: &'a mut R, depth: u32) -> super::Result { - self.get_raw_or_lookup(node, r, depth).map(Node::decoded) + self.get_raw_or_lookup(node, r, depth).map(|n| Node::decoded(&n)) } /// Indentation helper for `formal_all`. @@ -155,20 +155,20 @@ impl<'db> TrieDB<'db> { fn fmt_all(&self, node: Node, f: &mut fmt::Formatter, deepness: usize) -> fmt::Result { match node { Node::Leaf(slice, value) => try!(writeln!(f, "'{:?}: {:?}.", slice, value.pretty())), - Node::Extension(ref slice, item) => { + Node::Extension(ref slice, ref item) => { try!(write!(f, "'{:?} ", slice)); - if let Ok(node) = self.get_node(item, &mut NoOp, 0) { + if let Ok(node) = self.get_node(&*item, &mut NoOp, 0) { try!(self.fmt_all(node, f, deepness)); } }, Node::Branch(ref nodes, ref value) => { try!(writeln!(f, "")); - if let Some(v) = *value { + if let Some(ref v) = *value { try!(self.fmt_indent(f, deepness + 1)); try!(writeln!(f, "=: {:?}", v.pretty())) } for i in 0..16 { - match self.get_node(nodes[i], &mut NoOp, 0) { + match self.get_node(&*nodes[i], &mut NoOp, 0) { Ok(Node::Empty) => {}, Ok(n) => { try!(self.fmt_indent(f, deepness + 1)); @@ -190,11 +190,11 @@ impl<'db> TrieDB<'db> { } /// Return optional data for a key given as a `NibbleSlice`. Returns `None` if no data exists. - fn do_lookup<'key, R: 'key>(&'db self, key: &NibbleSlice<'key>, r: &'key mut R) -> super::Result> + fn do_lookup<'key, R: 'key>(&'db self, key: &NibbleSlice<'key>, r: &'key mut R) -> super::Result> where 'db: 'key, R: Recorder { let root_rlp = try!(self.root_data(r)); - self.get_from_node(root_rlp, key, r, 1) + self.get_from_node(&root_rlp, key, r, 1) } /// Recursible function to retrieve the value given a `node` and a partial `key`. `None` if no @@ -207,18 +207,23 @@ impl<'db> TrieDB<'db> { key: &NibbleSlice<'key>, r: &'key mut R, d: u32 - ) -> super::Result> where 'db: 'key, R: Recorder { + ) -> super::Result> where 'db: 'key, R: Recorder { match Node::decoded(node) { - Node::Leaf(ref slice, value) if key == slice => Ok(Some(value)), - Node::Extension(ref slice, item) if key.starts_with(slice) => { - let data = try!(self.get_raw_or_lookup(item, r, d)); - self.get_from_node(data, &key.mid(slice.len()), r, d + 1) + Node::Leaf(ref slice, ref value) if NibbleSlice::from_encoded(slice).0 == *key => Ok(Some(value.clone())), + Node::Extension(ref slice, ref item) => { + let slice = &NibbleSlice::from_encoded(slice).0; + if key.starts_with(slice) { + let data = try!(self.get_raw_or_lookup(&*item, r, d)); + self.get_from_node(&data, &key.mid(slice.len()), r, d + 1) + } else { + Ok(None) + } }, - Node::Branch(ref nodes, value) => match key.is_empty() { - true => Ok(value), + Node::Branch(ref nodes, ref value) => match key.is_empty() { + true => Ok(value.clone()), false => { - let node = try!(self.get_raw_or_lookup(nodes[key.at(0) as usize], r, d)); - self.get_from_node(node, &key.mid(1), r, d + 1) + let node = try!(self.get_raw_or_lookup(&*nodes[key.at(0) as usize], r, d)); + self.get_from_node(&node, &key.mid(1), r, d + 1) } }, _ => Ok(None) @@ -228,16 +233,16 @@ impl<'db> TrieDB<'db> { /// Given some node-describing data `node`, return the actual node RLP. /// This could be a simple identity operation in the case that the node is sufficiently small, but /// may require a database lookup. - fn get_raw_or_lookup(&'db self, node: &'db [u8], rec: &mut R, d: u32) -> super::Result<&'db [u8]> { + fn get_raw_or_lookup(&'db self, node: &'db [u8], rec: &mut R, d: u32) -> super::Result { // check if its sha3 + len let r = Rlp::new(node); match r.is_data() && r.size() == 32 { true => { let key = r.as_val::(); self.db.get(&key).ok_or_else(|| Box::new(TrieError::IncompleteDatabase(key))) - .map(|raw| { rec.record(&key, raw, d); raw }) + .map(|raw| { rec.record(&key, &raw, d); raw }) } - false => Ok(node) + false => Ok(DBValue::from_slice(node)) } } } @@ -251,12 +256,12 @@ enum Status { } #[derive(Clone, Eq, PartialEq)] -struct Crumb<'a> { - node: Node<'a>, +struct Crumb { + node: Node, status: Status, } -impl<'a> Crumb<'a> { +impl Crumb { /// Move on to next status in the node's sequence. fn increment(&mut self) { self.status = match (&self.status, &self.node) { @@ -273,7 +278,7 @@ impl<'a> Crumb<'a> { #[derive(Clone)] pub struct TrieDBIterator<'a> { db: &'a TrieDB<'a>, - trail: Vec>, + trail: Vec, key_nibbles: Bytes, } @@ -286,18 +291,18 @@ impl<'a> TrieDBIterator<'a> { key_nibbles: Vec::new(), }; - try!(db.root_data(&mut NoOp).and_then(|root| r.descend(root))); + try!(db.root_data(&mut NoOp).and_then(|root| r.descend(&root))); Ok(r) } /// Descend into a payload. - fn descend(&mut self, d: &'a [u8]) -> super::Result<()> { + fn descend(&mut self, d: &[u8]) -> super::Result<()> { self.trail.push(Crumb { status: Status::Entering, node: try!(self.db.get_node(d, &mut NoOp, 0)), }); match self.trail.last().expect("just pushed item; qed").node { - Node::Leaf(n, _) | Node::Extension(n, _) => { self.key_nibbles.extend(n.iter()); }, + Node::Leaf(ref n, _) | Node::Extension(ref n, _) => { self.key_nibbles.extend(NibbleSlice::from_encoded(n).0.iter()); }, _ => {} } @@ -325,7 +330,7 @@ impl<'a> Iterator for TrieDBIterator<'a> { match n { Node::Leaf(n, _) | Node::Extension(n, _) => { let l = self.key_nibbles.len(); - self.key_nibbles.truncate(l - n.len()); + self.key_nibbles.truncate(l - NibbleSlice::from_encoded(&*n).0.len()); }, Node::Branch(_, _) => { self.key_nibbles.pop(); }, _ => {} @@ -337,19 +342,19 @@ impl<'a> Iterator for TrieDBIterator<'a> { return Some(Ok((self.key(), v))); }, (Status::At, Node::Extension(_, d)) => { - if let Err(e) = self.descend(d) { + if let Err(e) = self.descend(&*d) { return Some(Err(e)); } // continue }, (Status::At, Node::Branch(_, _)) => {}, - (Status::AtChild(i), Node::Branch(children, _)) if children[i].len() > 0 => { + (Status::AtChild(i), Node::Branch(ref children, _)) if children[i].len() > 0 => { match i { 0 => self.key_nibbles.push(0), i => *self.key_nibbles.last_mut() .expect("pushed as 0; moves sequentially; removed afterwards; qed") = i as u8, } - if let Err(e) = self.descend(children[i]) { + if let Err(e) = self.descend(&*children[i]) { return Some(Err(e)); } // continue @@ -373,7 +378,7 @@ impl<'db> Trie for TrieDB<'db> { fn root(&self) -> &H256 { self.root } - fn get_recorded<'a, 'b, R: 'b>(&'a self, key: &'b [u8], rec: &'b mut R) -> super::Result> + fn get_recorded<'a, 'b, R: 'b>(&'a self, key: &'b [u8], rec: &'b mut R) -> super::Result> where 'a: 'b, R: Recorder { self.do_lookup(&NibbleSlice::new(key), rec) @@ -384,7 +389,7 @@ impl<'db> fmt::Debug for TrieDB<'db> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { try!(writeln!(f, "c={:?} [", self.hash_count)); let root_rlp = self.db.get(self.root).expect("Trie root not found!"); - try!(self.fmt_all(Node::decoded(root_rlp), f, 0)); + try!(self.fmt_all(Node::decoded(&root_rlp), f, 0)); writeln!(f, "]") } } @@ -395,7 +400,7 @@ fn iterator() { use super::TrieMut; use super::triedbmut::*; - let d = vec![ &b"A"[..], &b"AA"[..], &b"AB"[..], &b"B"[..] ]; + let d = vec![ DBValue::from_slice(b"A"), DBValue::from_slice(b"AA"), DBValue::from_slice(b"AB"), DBValue::from_slice(b"B") ]; let mut memdb = MemoryDB::new(); let mut root = H256::new(); @@ -407,6 +412,6 @@ fn iterator() { } let t = TrieDB::new(&memdb, &root).unwrap(); - assert_eq!(d.iter().map(|i|i.to_vec()).collect::>(), t.iter().unwrap().map(|x| x.unwrap().0).collect::>()); + assert_eq!(d.iter().map(|i| i.clone().to_vec()).collect::>(), t.iter().unwrap().map(|x| x.unwrap().0).collect::>()); assert_eq!(d, t.iter().unwrap().map(|x| x.unwrap().1).collect::>()); } diff --git a/util/src/trie/triedbmut.rs b/util/src/trie/triedbmut.rs index e99179e7a..f5940dd7b 100644 --- a/util/src/trie/triedbmut.rs +++ b/util/src/trie/triedbmut.rs @@ -18,12 +18,14 @@ use super::{TrieError, TrieMut}; use super::node::Node as RlpNode; +use super::node::NodeKey; -use ::{Bytes, HashDB, H256}; +use ::{HashDB, H256}; use ::bytes::ToPretty; use ::nibbleslice::NibbleSlice; use ::rlp::{Rlp, RlpStream, View, Stream}; use ::sha3::SHA3_NULL_RLP; +use hashdb::DBValue; use elastic_array::ElasticArray1024; @@ -72,14 +74,14 @@ enum Node { /// A leaf node contains the end of a key and a value. /// This key is encoded from a `NibbleSlice`, meaning it contains /// a flag indicating it is a leaf. - Leaf(Bytes, Bytes), + Leaf(NodeKey, DBValue), /// An extension contains a shared portion of a key and a child node. /// The shared portion is encoded from a `NibbleSlice` meaning it contains /// a flag indicating it is an extension. /// The child node is always a branch. - Extension(Bytes, NodeHandle), + Extension(NodeKey, NodeHandle), /// A branch has up to 16 children and an optional value. - Branch(Box<[Option; 16]>, Option) + Branch(Box<[Option; 16]>, Option) } impl Node { @@ -98,21 +100,18 @@ impl Node { fn from_rlp(rlp: &[u8], db: &HashDB, storage: &mut NodeStorage) -> Self { match RlpNode::decoded(rlp) { RlpNode::Empty => Node::Empty, - RlpNode::Leaf(k, v) => Node::Leaf(k.encoded(true), v.to_owned()), - RlpNode::Extension(partial, cb) => { - let key = partial.encoded(false); - - Node::Extension(key, Self::inline_or_hash(cb, db, storage)) + RlpNode::Leaf(k, v) => Node::Leaf(k, v), + RlpNode::Extension(key, cb) => { + Node::Extension(key, Self::inline_or_hash(&*cb, db, storage)) } - RlpNode::Branch(children_rlp, v) => { - let val = v.map(|x| x.to_owned()); + RlpNode::Branch(children_rlp, val) => { let mut children = empty_children(); for i in 0..16 { - let raw = children_rlp[i]; - let child_rlp = Rlp::new(raw); + let raw = &children_rlp[i]; + let child_rlp = Rlp::new(&*raw); if !child_rlp.is_empty() { - children[i] = Some(Self::inline_or_hash(raw, db, storage)); + children[i] = Some(Self::inline_or_hash(&*raw, db, storage)); } } @@ -134,13 +133,13 @@ impl Node { } Node::Leaf(partial, value) => { let mut stream = RlpStream::new_list(2); - stream.append(&partial); - stream.append(&value); + stream.append(&&*partial); + stream.append(&&*value); stream.drain() } Node::Extension(partial, child) => { let mut stream = RlpStream::new_list(2); - stream.append(&partial); + stream.append(&&*partial); child_cb(child, &mut stream); stream.drain() } @@ -154,7 +153,7 @@ impl Node { } } if let Some(value) = value { - stream.append(&value); + stream.append(&&*value); } else { stream.append_empty_data(); } @@ -276,7 +275,7 @@ impl<'a> Index<&'a StorageHandle> for NodeStorage { /// assert_eq!(*t.root(), ::util::sha3::SHA3_NULL_RLP); /// t.insert(b"foo", b"bar").unwrap(); /// assert!(t.contains(b"foo").unwrap()); -/// assert_eq!(t.get(b"foo").unwrap().unwrap(), b"bar"); +/// assert_eq!(t.get(b"foo").unwrap().unwrap(), DBValue::from_slice(b"bar")); /// t.remove(b"foo").unwrap(); /// assert!(!t.contains(b"foo").unwrap()); /// } @@ -338,7 +337,7 @@ impl<'a> TrieDBMut<'a> { // cache a node by hash fn cache(&mut self, hash: H256) -> super::Result { let node_rlp = try!(self.db.get(&hash).ok_or_else(|| Box::new(TrieError::IncompleteDatabase(hash)))); - let node = Node::from_rlp(node_rlp, &*self.db, &mut self.storage); + let node = Node::from_rlp(&node_rlp, &*self.db, &mut self.storage); Ok(self.storage.alloc(Stored::Cached(node, hash))) } @@ -367,7 +366,7 @@ impl<'a> TrieDBMut<'a> { } // walk the trie, attempting to find the key's node. - fn lookup<'x, 'key>(&'x self, partial: NibbleSlice<'key>, handle: &NodeHandle) -> super::Result> + fn lookup<'x, 'key>(&'x self, partial: NibbleSlice<'key>, handle: &NodeHandle) -> super::Result> where 'x: 'key { match *handle { @@ -376,7 +375,7 @@ impl<'a> TrieDBMut<'a> { Node::Empty => Ok(None), Node::Leaf(ref key, ref value) => { if NibbleSlice::from_encoded(key).0 == partial { - Ok(Some(value)) + Ok(Some(DBValue::from_slice(value))) } else { Ok(None) } @@ -391,7 +390,7 @@ impl<'a> TrieDBMut<'a> { } Node::Branch(ref children, ref value) => { if partial.is_empty() { - Ok(value.as_ref().map(|v| &v[..])) + Ok(value.as_ref().map(|v| DBValue::from_slice(v))) } else { let idx = partial.at(0); match children[idx as usize].as_ref() { @@ -405,28 +404,33 @@ impl<'a> TrieDBMut<'a> { } /// Return optional data for a key given as a `NibbleSlice`. Returns `None` if no data exists. - fn do_db_lookup<'x, 'key>(&'x self, hash: &H256, key: NibbleSlice<'key>) -> super::Result> + fn do_db_lookup<'x, 'key>(&'x self, hash: &H256, key: NibbleSlice<'key>) -> super::Result> where 'x: 'key { self.db.get(hash).ok_or_else(|| Box::new(TrieError::IncompleteDatabase(*hash))) - .and_then(|node_rlp| self.get_from_db_node(node_rlp, key)) + .and_then(|node_rlp| self.get_from_db_node(&node_rlp, key)) } /// Recursible function to retrieve the value given a `node` and a partial `key`. `None` if no /// value exists for the key. /// /// Note: Not a public API; use Trie trait functions. - fn get_from_db_node<'x, 'key>(&'x self, node: &'x [u8], key: NibbleSlice<'key>) -> super::Result> + fn get_from_db_node<'x, 'key>(&'x self, node: &'x [u8], key: NibbleSlice<'key>) -> super::Result> where 'x: 'key { match RlpNode::decoded(node) { - RlpNode::Leaf(ref slice, value) if &key == slice => Ok(Some(value)), - RlpNode::Extension(ref slice, item) if key.starts_with(slice) => { - self.get_from_db_node(try!(self.get_raw_or_lookup(item)), key.mid(slice.len())) + RlpNode::Leaf(ref slice, ref value) if NibbleSlice::from_encoded(slice).0 == key => Ok(Some(value.clone())), + RlpNode::Extension(ref slice, ref item) => { + let slice = &NibbleSlice::from_encoded(slice).0; + if key.starts_with(slice) { + self.get_from_db_node(&try!(self.get_raw_or_lookup(&*item)), key.mid(slice.len())) + } else { + Ok(None) + } }, - RlpNode::Branch(ref nodes, value) => match key.is_empty() { - true => Ok(value), - false => self.get_from_db_node(try!(self.get_raw_or_lookup(nodes[key.at(0) as usize])), key.mid(1)) + RlpNode::Branch(ref nodes, ref value) => match key.is_empty() { + true => Ok(value.clone()), + false => self.get_from_db_node(&try!(self.get_raw_or_lookup(&*nodes[key.at(0) as usize])), key.mid(1)) }, _ => Ok(None), } @@ -435,7 +439,7 @@ impl<'a> TrieDBMut<'a> { /// Given some node-describing data `node`, return the actual node RLP. /// This could be a simple identity operation in the case that the node is sufficiently small, but /// may require a database lookup. - fn get_raw_or_lookup<'x>(&'x self, node: &'x [u8]) -> super::Result<&'x [u8]> { + fn get_raw_or_lookup<'x>(&'x self, node: &'x [u8]) -> super::Result { // check if its sha3 + len let r = Rlp::new(node); match r.is_data() && r.size() == 32 { @@ -443,12 +447,12 @@ impl<'a> TrieDBMut<'a> { let key = r.as_val::(); self.db.get(&key).ok_or_else(|| Box::new(TrieError::IncompleteDatabase(key))) } - false => Ok(node) + false => Ok(DBValue::from_slice(node)) } } /// insert a key, value pair into the trie, creating new nodes if necessary. - fn insert_at(&mut self, handle: NodeHandle, partial: NibbleSlice, value: Bytes) -> super::Result<(StorageHandle, bool)> { + fn insert_at(&mut self, handle: NodeHandle, partial: NibbleSlice, value: DBValue) -> super::Result<(StorageHandle, bool)> { let h = match handle { NodeHandle::InMemory(h) => h, NodeHandle::Hash(h) => try!(self.cache(h)), @@ -463,7 +467,7 @@ impl<'a> TrieDBMut<'a> { /// the insertion inspector. #[cfg_attr(feature = "dev", allow(cyclomatic_complexity))] - fn insert_inspector(&mut self, node: Node, partial: NibbleSlice, value: Bytes) -> super::Result { + fn insert_inspector(&mut self, node: Node, partial: NibbleSlice, value: DBValue) -> super::Result { trace!(target: "trie", "augmented (partial: {:?}, value: {:?})", partial, value.pretty()); Ok(match node { @@ -898,7 +902,7 @@ impl<'a> TrieMut for TrieDBMut<'a> { } } - fn get<'x, 'key>(&'x self, key: &'key [u8]) -> super::Result> where 'x: 'key { + fn get<'x, 'key>(&'x self, key: &'key [u8]) -> super::Result> where 'x: 'key { self.lookup(NibbleSlice::new(key), &self.root_handle) } @@ -911,7 +915,7 @@ impl<'a> TrieMut for TrieDBMut<'a> { trace!(target: "trie", "insert: key={:?}, value={:?}", key.pretty(), value.pretty()); let root_handle = self.root_handle(); - let (new_handle, changed) = try!(self.insert_at(root_handle, NibbleSlice::new(key), value.to_owned())); + let (new_handle, changed) = try!(self.insert_at(root_handle, NibbleSlice::new(key), DBValue::from_slice(value))); trace!(target: "trie", "insert: altered trie={}", changed); self.root_handle = NodeHandle::InMemory(new_handle); @@ -1180,9 +1184,9 @@ mod tests { let mut root = H256::new(); let mut t = TrieDBMut::new(&mut memdb, &mut root); t.insert(&[0x01u8, 0x23], &[0x01u8, 0x23]).unwrap(); - assert_eq!(t.get(&[0x1, 0x23]).unwrap().unwrap(), &[0x1u8, 0x23]); + assert_eq!(t.get(&[0x1, 0x23]).unwrap().unwrap(), DBValue::from_slice(&[0x1u8, 0x23])); t.commit(); - assert_eq!(t.get(&[0x1, 0x23]).unwrap().unwrap(), &[0x1u8, 0x23]); + assert_eq!(t.get(&[0x1, 0x23]).unwrap().unwrap(), DBValue::from_slice(&[0x1u8, 0x23])); } #[test] @@ -1193,14 +1197,14 @@ mod tests { t.insert(&[0x01u8, 0x23], &[0x01u8, 0x23]).unwrap(); t.insert(&[0xf1u8, 0x23], &[0xf1u8, 0x23]).unwrap(); t.insert(&[0x81u8, 0x23], &[0x81u8, 0x23]).unwrap(); - assert_eq!(t.get(&[0x01, 0x23]).unwrap().unwrap(), &[0x01u8, 0x23]); - assert_eq!(t.get(&[0xf1, 0x23]).unwrap().unwrap(), &[0xf1u8, 0x23]); - assert_eq!(t.get(&[0x81, 0x23]).unwrap().unwrap(), &[0x81u8, 0x23]); + assert_eq!(t.get(&[0x01, 0x23]).unwrap().unwrap(), DBValue::from_slice(&[0x01u8, 0x23])); + assert_eq!(t.get(&[0xf1, 0x23]).unwrap().unwrap(), DBValue::from_slice(&[0xf1u8, 0x23])); + assert_eq!(t.get(&[0x81, 0x23]).unwrap().unwrap(), DBValue::from_slice(&[0x81u8, 0x23])); assert_eq!(t.get(&[0x82, 0x23]), Ok(None)); t.commit(); - assert_eq!(t.get(&[0x01, 0x23]).unwrap().unwrap(), &[0x01u8, 0x23]); - assert_eq!(t.get(&[0xf1, 0x23]).unwrap().unwrap(), &[0xf1u8, 0x23]); - assert_eq!(t.get(&[0x81, 0x23]).unwrap().unwrap(), &[0x81u8, 0x23]); + assert_eq!(t.get(&[0x01, 0x23]).unwrap().unwrap(), DBValue::from_slice(&[0x01u8, 0x23])); + assert_eq!(t.get(&[0xf1, 0x23]).unwrap().unwrap(), DBValue::from_slice(&[0xf1u8, 0x23])); + assert_eq!(t.get(&[0x81, 0x23]).unwrap().unwrap(), DBValue::from_slice(&[0x81u8, 0x23])); assert_eq!(t.get(&[0x82, 0x23]), Ok(None)); } From 0441babb5f665f8691babb69536e0f81f73cdea6 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Wed, 26 Oct 2016 13:55:41 +0200 Subject: [PATCH 66/77] Fix empty tags modif. (#2882) --- js/src/modals/EditMeta/editMeta.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/modals/EditMeta/editMeta.js b/js/src/modals/EditMeta/editMeta.js index aef0232a6..afcf9b127 100644 --- a/js/src/modals/EditMeta/editMeta.js +++ b/js/src/modals/EditMeta/editMeta.js @@ -137,7 +137,7 @@ export default class EditMeta extends Component { onTagsInputChange = (value) => { const { meta } = this.state; - const { tags } = meta || []; + const { tags = [] } = meta; const tokens = value.split(/[\s,;]+/); From c9298981f802c0d91290cb5443364dbe17ae9ec6 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Wed, 26 Oct 2016 13:57:54 +0200 Subject: [PATCH 67/77] Fix up informant. (#2865) Just ensure as many transactions/blocks are reported as possible. --- parity/informant.rs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/parity/informant.rs b/parity/informant.rs index 33ad54b3d..9b0b6c754 100644 --- a/parity/informant.rs +++ b/parity/informant.rs @@ -43,6 +43,7 @@ pub struct Informant { net: Option>, last_import: Mutex, skipped: AtomicUsize, + skipped_txs: AtomicUsize, } /// Format byte counts to standard denominations. @@ -80,6 +81,7 @@ impl Informant { net: net, last_import: Mutex::new(Instant::now()), skipped: AtomicUsize::new(0), + skipped_txs: AtomicUsize::new(0), } } @@ -178,13 +180,21 @@ impl ChainNotify for Informant { let mut last_import = self.last_import.lock(); let sync_state = self.sync.as_ref().map(|s| s.status().state); let importing = is_major_importing(sync_state, self.client.queue_info()); - if Instant::now() > *last_import + Duration::from_secs(1) && !importing { + + let ripe = Instant::now() > *last_import + Duration::from_secs(1) && !importing; + let txs_imported = imported.iter() + .take(imported.len() - if ripe {1} else {0}) + .filter_map(|h| self.client.block(BlockID::Hash(h.clone()))) + .map(|b| BlockView::new(&b).transactions_count()) + .sum(); + + if ripe { if let Some(block) = imported.last().and_then(|h| self.client.block(BlockID::Hash(*h))) { let view = BlockView::new(&block); let header = view.header(); let tx_count = view.transactions_count(); let size = block.len(); - let skipped = self.skipped.load(AtomicOrdering::Relaxed); + let (skipped, skipped_txs) = (self.skipped.load(AtomicOrdering::Relaxed) + imported.len() - 1, self.skipped.load(AtomicOrdering::Relaxed) + txs_imported); info!(target: "import", "Imported {} {} ({} txs, {} Mgas, {} ms, {} KiB){}", Colour::White.bold().paint(format!("#{}", header.number())), Colour::White.bold().paint(format!("{}", header.hash())), @@ -192,13 +202,22 @@ impl ChainNotify for Informant { Colour::Yellow.bold().paint(format!("{:.2}", header.gas_used().low_u64() as f32 / 1000000f32)), Colour::Purple.bold().paint(format!("{:.2}", duration as f32 / 1000000f32)), Colour::Blue.bold().paint(format!("{:.2}", size as f32 / 1024f32)), - if skipped > 0 { format!(" + another {} block(s)", Colour::Red.bold().paint(format!("{}", skipped))) } else { String::new() } + if skipped > 0 { + format!(" + another {} block(s) containing {} tx(s)", + Colour::Red.bold().paint(format!("{}", skipped)), + Colour::Red.bold().paint(format!("{}", skipped_txs)) + ) + } else { + String::new() + } ); + self.skipped.store(0, AtomicOrdering::Relaxed); + self.skipped_txs.store(0, AtomicOrdering::Relaxed); *last_import = Instant::now(); } - self.skipped.store(0, AtomicOrdering::Relaxed); } else { self.skipped.fetch_add(imported.len(), AtomicOrdering::Relaxed); + self.skipped_txs.fetch_add(txs_imported, AtomicOrdering::Relaxed); } } } From cdc348d9554b7948834a56d8f635deeebf615bc9 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Wed, 26 Oct 2016 12:03:52 +0000 Subject: [PATCH 68/77] [ci skip] js-precompiled 20161026-120247 --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 9df7200e8..6592df2e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1211,7 +1211,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#1c5409c9102b62600f9e5e2894221827b24721f8" +source = "git+https://github.com/ethcore/js-precompiled.git#10abc6955255a004351239cd6bd0a917dfe5f443" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] From 909fb1d54e880edbf108065009cccbedf9d256ca Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Wed, 26 Oct 2016 12:15:18 +0000 Subject: [PATCH 69/77] [ci skip] js-precompiled 20161026-121420 --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 6592df2e2..ae87fc1dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1211,7 +1211,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#10abc6955255a004351239cd6bd0a917dfe5f443" +source = "git+https://github.com/ethcore/js-precompiled.git#a9f4d6f0ff2a084cb5425b75430dd0ad45c21700" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] From af27bfe868e1eb123bdb372387fa511aa7f044c0 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Wed, 26 Oct 2016 14:59:45 +0200 Subject: [PATCH 70/77] Fixes CI JS precompiled build (#2886) * Add inject to "bundle everything" list * Fixes the `build-server` script // Updates Webpack config (#2872) * New Webpack config file for libraries * Added `parity-utils` path * Removed parity in CommonChunks prod * Fixes CI build --- js/package.json | 2 +- js/webpack.config.js | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/js/package.json b/js/package.json index 8992c7fa6..bafb3efcd 100644 --- a/js/package.json +++ b/js/package.json @@ -28,7 +28,7 @@ "build:lib": "webpack --config webpack.libraries --progress", "build:dll": "webpack --config webpack.vendor --progress", - "ci:build": "npm run ci:build:dll && npm run ci:build:dll && npm run ci:build:app", + "ci:build": "npm run ci:build:dll && npm run ci:build:app && npm run ci:build:lib", "ci:build:app": "NODE_ENV=production webpack", "ci:build:lib": "NODE_ENV=production webpack --config webpack.libraries", "ci:build:dll": "NODE_ENV=production webpack --config webpack.vendor", diff --git a/js/webpack.config.js b/js/webpack.config.js index c72ef8936..51337b9ef 100644 --- a/js/webpack.config.js +++ b/js/webpack.config.js @@ -40,10 +40,6 @@ module.exports = { 'registry': ['./dapps/registry.js'], 'signaturereg': ['./dapps/signaturereg.js'], 'tokenreg': ['./dapps/tokenreg.js'], - // library - 'parity': ['./parity.js'], - 'inject': ['./web3.js'], - 'web3': ['./web3.js'], // app 'index': ['./index.js'] }, From c36202fcf50ed03a79f9f9c280d46afa33be73d3 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Wed, 26 Oct 2016 13:19:04 +0000 Subject: [PATCH 71/77] [ci skip] js-precompiled 20161026-131806 --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index ae87fc1dd..2ffb29a7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1211,7 +1211,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#a9f4d6f0ff2a084cb5425b75430dd0ad45c21700" +source = "git+https://github.com/ethcore/js-precompiled.git#7e761cb85def0ade260e60cc05f697a0bf845ac9" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] From f024acd3295555458a8aa26c3b3dc124498a1f88 Mon Sep 17 00:00:00 2001 From: Arkadiy Paronyan Date: Wed, 26 Oct 2016 16:14:13 +0200 Subject: [PATCH 72/77] More snapshot fixes and optimizations (#2883) * More snapshot fixes and optimizations * db drop --- ethcore/res/ethereum/frontier.json | 3 +-- ethcore/src/snapshot/mod.rs | 41 ++++++------------------------ ethcore/src/snapshot/service.rs | 3 ++- 3 files changed, 11 insertions(+), 36 deletions(-) diff --git a/ethcore/res/ethereum/frontier.json b/ethcore/res/ethereum/frontier.json index 2ad0f0bec..d5f57defd 100644 --- a/ethcore/res/ethereum/frontier.json +++ b/ethcore/res/ethereum/frontier.json @@ -164,8 +164,7 @@ "enode://a979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@52.16.188.185:30303", "enode://de471bccee3d042261d52e9bff31458daecc406142b401d4cd848f677479f73104b9fdeb090af9583d3391b7f10cb2ba9e26865dd5fca4fcdc0fb1e3b723c786@54.94.239.50:30303", "enode://1118980bf48b0a3640bdba04e0fe78b1add18e1cd99bf22d53daac1fd9972ad650df52176e7c7d89d1114cfef2bc23a2959aa54998a46afcf7d91809f0855082@52.74.57.123:30303", - "enode://4cd540b2c3292e17cff39922e864094bf8b0741fcc8c5dcea14957e389d7944c70278d872902e3d0345927f621547efa659013c400865485ab4bfa0c6596936f@zero.parity.io:30303", - "enode://cc92c4c40d612a10c877ca023ef0496c843fbc92b6c6c0d55ce0b863d51d821c4bd70daebb54324a6086374e6dc05708fed39862b275f169cb678e655da9d07d@136.243.154.246:30303" + "enode://4cd540b2c3292e17cff39922e864094bf8b0741fcc8c5dcea14957e389d7944c70278d872902e3d0345927f621547efa659013c400865485ab4bfa0c6596936f@138.201.144.135:30303" ], "accounts": { "0000000000000000000000000000000000000001": { "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } }, diff --git a/ethcore/src/snapshot/mod.rs b/ethcore/src/snapshot/mod.rs index 223d769d2..4fa00f771 100644 --- a/ethcore/src/snapshot/mod.rs +++ b/ethcore/src/snapshot/mod.rs @@ -30,7 +30,6 @@ use ids::BlockID; use views::BlockView; use util::{Bytes, Hashable, HashDB, DBValue, snappy, U256, Uint}; -use util::memorydb::MemoryDB; use util::Mutex; use util::hash::{FixedHash, H256}; use util::journaldb::{self, Algorithm, JournalDB}; @@ -47,7 +46,7 @@ use self::io::SnapshotWriter; use super::state_db::StateDB; use super::state::Account as StateAccount; -use crossbeam::{scope, ScopedJoinHandle}; +use crossbeam::scope; use rand::{Rng, OsRng}; pub use self::error::Error; @@ -421,38 +420,14 @@ impl StateRebuilder { // new code contained within this chunk. let mut chunk_code = HashMap::new(); - // build account tries in parallel. - // Todo [rob] keep a thread pool around so we don't do this per-chunk. - try!(scope(|scope| { - let mut handles = Vec::new(); - for (account_chunk, out_pairs_chunk) in account_fat_rlps.chunks(chunk_size).zip(pairs.chunks_mut(chunk_size)) { - let code_map = &self.code_map; - let handle: ScopedJoinHandle> = scope.spawn(move || { - let mut db = MemoryDB::new(); - let status = try!(rebuild_accounts(&mut db, account_chunk, out_pairs_chunk, code_map)); - - trace!(target: "snapshot", "thread rebuilt {} account tries", account_chunk.len()); - Ok((db, status)) - }); - - handles.push(handle); + for (account_chunk, out_pairs_chunk) in account_fat_rlps.chunks(chunk_size).zip(pairs.chunks_mut(chunk_size)) { + let code_map = &self.code_map; + let status = try!(rebuild_accounts(self.db.as_hashdb_mut(), account_chunk, out_pairs_chunk, code_map)); + chunk_code.extend(status.new_code); + for (addr_hash, code_hash) in status.missing_code { + self.missing_code.entry(code_hash).or_insert_with(Vec::new).push(addr_hash); } - - // consolidate all edits into the main overlay. - for handle in handles { - let (thread_db, status): (MemoryDB, _) = try!(handle.join()); - self.db.consolidate(thread_db); - - chunk_code.extend(status.new_code); - - for (addr_hash, code_hash) in status.missing_code { - self.missing_code.entry(code_hash).or_insert_with(Vec::new).push(addr_hash); - } - } - - Ok::<_, ::error::Error>(()) - })); - + } // patch up all missing code. must be done after collecting all new missing code entries. for (code_hash, code) in chunk_code { for addr_hash in self.missing_code.remove(&code_hash).unwrap_or_else(Vec::new) { diff --git a/ethcore/src/snapshot/service.rs b/ethcore/src/snapshot/service.rs index b3aaa017e..9b66a5cdc 100644 --- a/ethcore/src/snapshot/service.rs +++ b/ethcore/src/snapshot/service.rs @@ -497,7 +497,8 @@ impl Service { match is_done { true => { try!(db.flush().map_err(::util::UtilError::SimpleString)); - self.finalize_restoration(&mut *restoration) + drop(db); + return self.finalize_restoration(&mut *restoration); }, false => Ok(()) } From 6901d087dd0f8d496d1880f993ad043fca5f1227 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Wed, 26 Oct 2016 14:16:03 +0000 Subject: [PATCH 73/77] [ci skip] js-precompiled 20161026-141507 --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 2ffb29a7b..fab068c80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1211,7 +1211,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#7e761cb85def0ade260e60cc05f697a0bf845ac9" +source = "git+https://github.com/ethcore/js-precompiled.git#49ee3f2dbcccca5f5147d8db45be7e74ba5d0735" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] From a1745624ce046dab9924c0876dc93830650e6096 Mon Sep 17 00:00:00 2001 From: keorn Date: Wed, 26 Oct 2016 15:27:38 +0100 Subject: [PATCH 74/77] enable suicide test (#2893) --- ethcore/src/json_tests/homestead_chain.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ethcore/src/json_tests/homestead_chain.rs b/ethcore/src/json_tests/homestead_chain.rs index d92b8a7f1..37a9d0a21 100644 --- a/ethcore/src/json_tests/homestead_chain.rs +++ b/ethcore/src/json_tests/homestead_chain.rs @@ -37,6 +37,5 @@ declare_test!{BlockchainTests_Homestead_bcUncleTest, "BlockchainTests/Homestead/ declare_test!{BlockchainTests_Homestead_bcValidBlockTest, "BlockchainTests/Homestead/bcValidBlockTest"} declare_test!{BlockchainTests_Homestead_bcWalletTest, "BlockchainTests/Homestead/bcWalletTest"} declare_test!{BlockchainTests_Homestead_bcShanghaiLove, "BlockchainTests/Homestead/bcShanghaiLove"} -// TODO [ToDr] uncomment as soon as eip150 tests are merged to develop branch of ethereum/tests -// declare_test!{BlockchainTests_Homestead_bcSuicideIssue, "BlockchainTests/Homestead/bcSuicideIssue"} +declare_test!{BlockchainTests_Homestead_bcSuicideIssue, "BlockchainTests/Homestead/bcSuicideIssue"} declare_test!{BlockchainTests_Homestead_bcExploitTest, "BlockchainTests/Homestead/bcExploitTest"} From 99fab5cdb9ae4de53e3b9fae83407aa34952850d Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Wed, 26 Oct 2016 16:27:53 +0200 Subject: [PATCH 75/77] Fix Webpack, again (#2895) * Add inject to "bundle everything" list * Fixes the `build-server` script // Updates Webpack config (#2872) * New Webpack config file for libraries * Added `parity-utils` path * Removed parity in CommonChunks prod * Fixes CI build * Re-Add Libs in Webpack Config --- js/webpack.config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/js/webpack.config.js b/js/webpack.config.js index 51337b9ef..d253b6592 100644 --- a/js/webpack.config.js +++ b/js/webpack.config.js @@ -40,6 +40,10 @@ module.exports = { 'registry': ['./dapps/registry.js'], 'signaturereg': ['./dapps/signaturereg.js'], 'tokenreg': ['./dapps/tokenreg.js'], + // library + 'inject': ['./web3.js'], + 'web3': ['./web3.js'], + 'parity': ['./parity.js'], // app 'index': ['./index.js'] }, From 215c82d744f772d2170ed3256b4382bb087dea2d Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Wed, 26 Oct 2016 14:43:01 +0000 Subject: [PATCH 76/77] [ci skip] js-precompiled 20161026-144207 --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index fab068c80..fdb80f602 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1211,7 +1211,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#49ee3f2dbcccca5f5147d8db45be7e74ba5d0735" +source = "git+https://github.com/ethcore/js-precompiled.git#9f8baa9d0e54056c41a842b351597d0565beda98" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] From e449477f23000c1a66fa8baa40ae295231b38230 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Wed, 26 Oct 2016 17:04:48 +0200 Subject: [PATCH 77/77] Add mising imgages for local dapps (#2890) --- js/src/views/Dapps/Summary/summary.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/src/views/Dapps/Summary/summary.js b/js/src/views/Dapps/Summary/summary.js index 1989386ea..9cf6a046b 100644 --- a/js/src/views/Dapps/Summary/summary.js +++ b/js/src/views/Dapps/Summary/summary.js @@ -39,8 +39,8 @@ export default class Summary extends Component { } const url = `/app/${app.builtin ? 'global' : 'local'}/${app.url || app.id}`; - const image = app.image - ? + const image = app.image || app.iconUrl + ? :
 
; return (