Merge branch 'master' into lightrpc

This commit is contained in:
Robert Habermeier 2017-02-04 22:30:20 +01:00
commit 7c9064c856
112 changed files with 3085 additions and 1116 deletions

53
Cargo.lock generated
View File

@ -28,7 +28,7 @@ dependencies = [
"fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)",
"isatty 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
@ -106,6 +106,11 @@ dependencies = [
"syntex_syntax 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "base32"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bigint"
version = "1.0.0"
@ -414,6 +419,7 @@ dependencies = [
name = "ethcore-dapps"
version = "1.6.0"
dependencies = [
"base32 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"ethcore-devtools 1.6.0",
@ -422,7 +428,7 @@ dependencies = [
"fetch 0.1.0",
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)",
"jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-http-server 7.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
@ -595,11 +601,12 @@ dependencies = [
"ethsync 1.6.0",
"fetch 0.1.0",
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-http-server 7.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-ipc-server 1.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-macros 0.2.0 (git+https://github.com/ethcore/jsonrpc.git)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"order-stat 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-reactor 0.1.0",
"parity-updater 1.6.0",
"rlp 0.1.0",
@ -622,7 +629,7 @@ dependencies = [
"ethcore-io 1.6.0",
"ethcore-rpc 1.6.0",
"ethcore-util 1.6.0",
"jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-ui 1.6.0",
@ -642,7 +649,7 @@ dependencies = [
"ethcore-ipc-nano 1.6.0",
"ethcore-util 1.6.0",
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-macros 0.2.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-tcp-server 1.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1005,8 +1012,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "jsonrpc-core"
version = "5.0.0"
source = "git+https://github.com/ethcore/jsonrpc.git#5eeee0980e4d2682a831c633fa03a8af99e0d68c"
version = "5.1.0"
source = "git+https://github.com/ethcore/jsonrpc.git#d179ce34d8da8ea1cd67e93a5b4cb1e30f48c156"
dependencies = [
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1019,11 +1026,10 @@ dependencies = [
[[package]]
name = "jsonrpc-http-server"
version = "7.0.0"
source = "git+https://github.com/ethcore/jsonrpc.git#5eeee0980e4d2682a831c633fa03a8af99e0d68c"
source = "git+https://github.com/ethcore/jsonrpc.git#d179ce34d8da8ea1cd67e93a5b4cb1e30f48c156"
dependencies = [
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)",
"jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1032,11 +1038,11 @@ dependencies = [
[[package]]
name = "jsonrpc-ipc-server"
version = "1.0.0"
source = "git+https://github.com/ethcore/jsonrpc.git#5eeee0980e4d2682a831c633fa03a8af99e0d68c"
source = "git+https://github.com/ethcore/jsonrpc.git#d179ce34d8da8ea1cd67e93a5b4cb1e30f48c156"
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)",
"jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"mio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1048,21 +1054,19 @@ dependencies = [
[[package]]
name = "jsonrpc-macros"
version = "0.2.0"
source = "git+https://github.com/ethcore/jsonrpc.git#5eeee0980e4d2682a831c633fa03a8af99e0d68c"
source = "git+https://github.com/ethcore/jsonrpc.git#d179ce34d8da8ea1cd67e93a5b4cb1e30f48c156"
dependencies = [
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)",
"serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "jsonrpc-tcp-server"
version = "1.0.0"
source = "git+https://github.com/ethcore/jsonrpc.git#5eeee0980e4d2682a831c633fa03a8af99e0d68c"
source = "git+https://github.com/ethcore/jsonrpc.git#d179ce34d8da8ea1cd67e93a5b4cb1e30f48c156"
dependencies = [
"env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1481,6 +1485,11 @@ dependencies = [
"user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "order-stat"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "owning_ref"
version = "0.2.2"
@ -1532,7 +1541,7 @@ dependencies = [
"ethcore-signer 1.6.0",
"ethcore-util 1.6.0",
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1563,7 +1572,7 @@ dependencies = [
[[package]]
name = "parity-ui-precompiled"
version = "1.4.0"
source = "git+https://github.com/ethcore/js-precompiled.git#416d00db677b8219f7548bb4dfa2f25c4b19f36e"
source = "git+https://github.com/ethcore/js-precompiled.git#4110b5bc85a15ae3f0b5c02b1c3caf8423f51b50"
dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -2473,6 +2482,7 @@ dependencies = [
"checksum app_dirs 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b7d1c0d48a81bbb13043847f957971f4d87c81542d80ece5e84ba3cba4058fd4"
"checksum arrayvec 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "16e3bdb2f54b3ace0285975d59a97cf8ed3855294b2b6bc651fcf22a9c352975"
"checksum aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07d344974f0a155f091948aa389fb1b912d3a58414fbdb9c8d446d193ee3496a"
"checksum base32 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1b9605ba46d61df0410d8ac686b0007add8172eba90e8e909c347856fe794d8c"
"checksum bigint 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2311bcd71b281e142a095311c22509f0d6bcd87b3000d7dbaa810929b9d6f6ae"
"checksum bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9bf6104718e80d7b26a68fdbacff3481cfc05df670821affc7e9cbc1884400c"
"checksum bit-vec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5b97c2c8e8bbb4251754f559df8af22fb264853c7d009084a576cdf12565089d"
@ -2525,7 +2535,7 @@ dependencies = [
"checksum itertools 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)" = "d95557e7ba6b71377b0f2c3b3ae96c53f1b75a926a6901a500f557a370af730a"
"checksum itoa 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ae3088ea4baeceb0284ee9eea42f591226e6beaecf65373e41b38d95a1b8e7a1"
"checksum itoa 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "91fd9dc2c587067de817fec4ad355e3818c3d893a78cab32a0a474c7a15bb8d5"
"checksum jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
"checksum jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
"checksum jsonrpc-http-server 7.0.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
"checksum jsonrpc-ipc-server 1.0.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
"checksum jsonrpc-macros 0.2.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
@ -2574,6 +2584,7 @@ dependencies = [
"checksum ole32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2c49021782e5233cd243168edfa8037574afed4eba4bbaf538b3d8d1789d8c"
"checksum openssl 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "12be61c7eaa23228316ff02c39807e4c1b1af84ba81420f19fd58dade304b25c"
"checksum openssl-sys 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d2845e841700e7b04282ceaa115407ea84e0db918ae689ad9ceb6f06fa6046bd"
"checksum order-stat 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "efa535d5117d3661134dbf1719b6f0ffe06f2375843b13935db186cd094105eb"
"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>"

View File

@ -23,6 +23,7 @@ serde = "0.8"
serde_json = "0.8"
linked-hash-map = "0.3"
parity-dapps-glue = "1.4"
base32 = "0.3"
mime = "0.2"
mime_guess = "1.6.1"
time = "0.1.35"

View File

@ -123,6 +123,7 @@ impl server::Handler<net::HttpStream> for RestApiRouter {
return Next::write();
}
// TODO [ToDr] Consider using `path.app_params` instead
let url = extract_url(&request);
if url.is_none() {
// Just return 404 if we can't parse URL

View File

@ -32,8 +32,8 @@ pub mod manifest;
extern crate parity_ui;
pub const HOME_PAGE: &'static str = "home";
pub const DAPPS_DOMAIN: &'static str = ".parity";
pub const HOME_PAGE: &'static str = "parity";
pub const DAPPS_DOMAIN: &'static str = ".web3.site";
pub const RPC_PATH: &'static str = "rpc";
pub const API_PATH: &'static str = "api";
pub const UTILS_PATH: &'static str = "parity-utils";

View File

@ -22,6 +22,7 @@ use std::collections::BTreeMap;
#[derive(Debug, PartialEq, Default, Clone)]
pub struct EndpointPath {
pub app_id: String,
pub app_params: Vec<String>,
pub host: String,
pub port: u16,
pub using_dapps_domains: bool,

View File

@ -19,6 +19,7 @@
#![warn(missing_docs)]
#![cfg_attr(feature="nightly", plugin(clippy))]
extern crate base32;
extern crate hyper;
extern crate time;
extern crate url as url_lib;
@ -69,9 +70,10 @@ use std::sync::{Arc, Mutex};
use std::net::SocketAddr;
use std::collections::HashMap;
use ethcore_rpc::Metadata;
use ethcore_rpc::{Metadata};
use fetch::{Fetch, Client as FetchClient};
use hash_fetch::urlhint::ContractClient;
use jsonrpc_core::Middleware;
use jsonrpc_core::reactor::RpcHandler;
use router::auth::{Authorization, NoAuth, HttpBasicAuth};
use parity_reactor::Remote;
@ -91,11 +93,11 @@ impl<F> SyncStatus for F where F: Fn() -> bool + Send + Sync {
/// Validates Web Proxy tokens
pub trait WebProxyTokens: Send + Sync {
/// Should return true if token is a valid web proxy access token.
fn is_web_proxy_token_valid(&self, token: &String) -> bool;
fn is_web_proxy_token_valid(&self, token: &str) -> bool;
}
impl<F> WebProxyTokens for F where F: Fn(String) -> bool + Send + Sync {
fn is_web_proxy_token_valid(&self, token: &String) -> bool { self(token.to_owned()) }
fn is_web_proxy_token_valid(&self, token: &str) -> bool { self(token.to_owned()) }
}
/// Webapps HTTP+RPC server build.
@ -178,7 +180,7 @@ impl<T: Fetch> ServerBuilder<T> {
/// Asynchronously start server with no authentication,
/// returns result with `Server` handle on success or an error.
pub fn start_unsecured_http(self, addr: &SocketAddr, handler: RpcHandler<Metadata>) -> Result<Server, ServerError> {
pub fn start_unsecured_http<S: Middleware<Metadata>>(self, addr: &SocketAddr, handler: RpcHandler<Metadata, S>) -> Result<Server, ServerError> {
let fetch = self.fetch_client()?;
Server::start_http(
addr,
@ -198,7 +200,7 @@ impl<T: Fetch> ServerBuilder<T> {
/// Asynchronously start server with `HTTP Basic Authentication`,
/// return result with `Server` handle on success or an error.
pub fn start_basic_auth_http(self, addr: &SocketAddr, username: &str, password: &str, handler: RpcHandler<Metadata>) -> Result<Server, ServerError> {
pub fn start_basic_auth_http<S: Middleware<Metadata>>(self, addr: &SocketAddr, username: &str, password: &str, handler: RpcHandler<Metadata, S>) -> Result<Server, ServerError> {
let fetch = self.fetch_client()?;
Server::start_http(
addr,
@ -257,11 +259,11 @@ impl Server {
}
}
fn start_http<A: Authorization + 'static, F: Fetch>(
fn start_http<A: Authorization + 'static, F: Fetch, T: Middleware<Metadata>>(
addr: &SocketAddr,
hosts: Option<Vec<String>>,
authorization: A,
handler: RpcHandler<Metadata>,
handler: RpcHandler<Metadata, T>,
dapps_path: PathBuf,
extra_dapps: Vec<PathBuf>,
signer_address: Option<(String, u16)>,
@ -409,6 +411,6 @@ mod util_tests {
// then
assert_eq!(none, Vec::<String>::new());
assert_eq!(some, vec!["http://home.parity".to_owned(), "http://127.0.0.1:18180".into()]);
assert_eq!(some, vec!["http://parity.web3.site".to_owned(), "http://127.0.0.1:18180".into()]);
}
}

View File

@ -252,6 +252,7 @@ fn should_extract_path_with_appid() {
prefix: None,
path: EndpointPath {
app_id: "app".to_owned(),
app_params: vec![],
host: "".to_owned(),
port: 8080,
using_dapps_domains: true,

View File

@ -97,9 +97,7 @@ impl<A: Authorization + 'static> server::Handler<HttpStream> for Router<A> {
=>
{
trace!(target: "dapps", "Redirecting to correct web request: {:?}", referer_url);
// TODO [ToDr] Some nice util for this!
let using_domain = if referer.using_dapps_domains { 0 } else { 1 };
let len = cmp::min(referer_url.path.len(), using_domain + 3); // token + protocol + hostname
let len = cmp::min(referer_url.path.len(), 2); // /web/<encoded>/
let base = referer_url.path[..len].join("/");
let requested = url.map(|u| u.path.join("/")).unwrap_or_default();
Redirection::boxed(&format!("/{}/{}", base, requested))
@ -262,20 +260,27 @@ fn extract_endpoint(url: &Option<Url>) -> (Option<EndpointPath>, SpecialEndpoint
match *url {
Some(ref url) => match url.host {
Host::Domain(ref domain) if domain.ends_with(DAPPS_DOMAIN) => {
let len = domain.len() - DAPPS_DOMAIN.len();
let id = domain[0..len].to_owned();
let id = &domain[0..(domain.len() - DAPPS_DOMAIN.len())];
let (id, params) = if let Some(split) = id.rfind('.') {
let (params, id) = id.split_at(split);
(id[1..].to_owned(), [params.to_owned()].into_iter().chain(&url.path).cloned().collect())
} else {
(id.to_owned(), url.path.clone())
};
(Some(EndpointPath {
app_id: id,
app_params: params,
host: domain.clone(),
port: url.port,
using_dapps_domains: true,
}), special_endpoint(url))
},
_ if url.path.len() > 1 => {
let id = url.path[0].clone();
let id = url.path[0].to_owned();
(Some(EndpointPath {
app_id: id.clone(),
app_id: id,
app_params: url.path[1..].to_vec(),
host: format!("{}", url.host),
port: url.port,
using_dapps_domains: false,
@ -296,6 +301,7 @@ fn should_extract_endpoint() {
extract_endpoint(&Url::parse("http://localhost:8080/status/index.html").ok()),
(Some(EndpointPath {
app_id: "status".to_owned(),
app_params: vec!["index.html".to_owned()],
host: "localhost".to_owned(),
port: 8080,
using_dapps_domains: false,
@ -307,6 +313,7 @@ fn should_extract_endpoint() {
extract_endpoint(&Url::parse("http://localhost:8080/rpc/").ok()),
(Some(EndpointPath {
app_id: "rpc".to_owned(),
app_params: vec!["".to_owned()],
host: "localhost".to_owned(),
port: 8080,
using_dapps_domains: false,
@ -314,10 +321,11 @@ fn should_extract_endpoint() {
);
assert_eq!(
extract_endpoint(&Url::parse("http://my.status.parity/parity-utils/inject.js").ok()),
extract_endpoint(&Url::parse("http://my.status.web3.site/parity-utils/inject.js").ok()),
(Some(EndpointPath {
app_id: "my.status".to_owned(),
host: "my.status.parity".to_owned(),
app_id: "status".to_owned(),
app_params: vec!["my".to_owned(), "parity-utils".into(), "inject.js".into()],
host: "my.status.web3.site".to_owned(),
port: 80,
using_dapps_domains: true,
}), SpecialEndpoint::Utils)
@ -325,10 +333,11 @@ fn should_extract_endpoint() {
// By Subdomain
assert_eq!(
extract_endpoint(&Url::parse("http://my.status.parity/test.html").ok()),
extract_endpoint(&Url::parse("http://status.web3.site/test.html").ok()),
(Some(EndpointPath {
app_id: "my.status".to_owned(),
host: "my.status.parity".to_owned(),
app_id: "status".to_owned(),
app_params: vec!["test.html".to_owned()],
host: "status.web3.site".to_owned(),
port: 80,
using_dapps_domains: true,
}), SpecialEndpoint::None)
@ -336,10 +345,11 @@ fn should_extract_endpoint() {
// RPC by subdomain
assert_eq!(
extract_endpoint(&Url::parse("http://my.status.parity/rpc/").ok()),
extract_endpoint(&Url::parse("http://my.status.web3.site/rpc/").ok()),
(Some(EndpointPath {
app_id: "my.status".to_owned(),
host: "my.status.parity".to_owned(),
app_id: "status".to_owned(),
app_params: vec!["my".to_owned(), "rpc".into(), "".into()],
host: "my.status.web3.site".to_owned(),
port: 80,
using_dapps_domains: true,
}), SpecialEndpoint::Rpc)
@ -347,10 +357,11 @@ fn should_extract_endpoint() {
// API by subdomain
assert_eq!(
extract_endpoint(&Url::parse("http://my.status.parity/api/").ok()),
extract_endpoint(&Url::parse("http://my.status.web3.site/api/").ok()),
(Some(EndpointPath {
app_id: "my.status".to_owned(),
host: "my.status.parity".to_owned(),
app_id: "status".to_owned(),
app_params: vec!["my".to_owned(), "api".into(), "".into()],
host: "my.status.web3.site".to_owned(),
port: 80,
using_dapps_domains: true,
}), SpecialEndpoint::Api)

View File

@ -18,11 +18,15 @@ use std::sync::{Arc, Mutex};
use hyper;
use ethcore_rpc::{Metadata, Origin};
use jsonrpc_core::Middleware;
use jsonrpc_core::reactor::RpcHandler;
use jsonrpc_http_server::{Rpc, ServerHandler, PanicHandler, AccessControlAllowOrigin, HttpMetaExtractor};
use endpoint::{Endpoint, EndpointPath, Handler};
pub fn rpc(handler: RpcHandler<Metadata>, panic_handler: Arc<Mutex<Option<Box<Fn() -> () + Send>>>>) -> Box<Endpoint> {
pub fn rpc<T: Middleware<Metadata>>(
handler: RpcHandler<Metadata, T>,
panic_handler: Arc<Mutex<Option<Box<Fn() -> () + Send>>>>,
) -> Box<Endpoint> {
Box::new(RpcEndpoint {
handler: handler,
meta_extractor: Arc::new(MetadataExtractor),
@ -33,15 +37,15 @@ pub fn rpc(handler: RpcHandler<Metadata>, panic_handler: Arc<Mutex<Option<Box<Fn
})
}
struct RpcEndpoint {
handler: RpcHandler<Metadata>,
struct RpcEndpoint<T: Middleware<Metadata>> {
handler: RpcHandler<Metadata, T>,
meta_extractor: Arc<HttpMetaExtractor<Metadata>>,
panic_handler: Arc<Mutex<Option<Box<Fn() -> () + Send>>>>,
cors_domain: Option<Vec<AccessControlAllowOrigin>>,
allowed_hosts: Option<Vec<String>>,
}
impl Endpoint for RpcEndpoint {
impl<T: Middleware<Metadata>> Endpoint for RpcEndpoint<T> {
fn to_async_handler(&self, _path: EndpointPath, control: hyper::Control) -> Box<Handler> {
let panic_handler = PanicHandler { handler: self.panic_handler.clone() };
Box::new(ServerHandler::new(

View File

@ -143,7 +143,7 @@ fn should_return_signer_port_cors_headers_for_home_parity() {
"\
POST /api/ping HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Origin: http://home.parity\r\n\
Origin: http://parity.web3.site\r\n\
Connection: close\r\n\
\r\n\
{}
@ -153,8 +153,8 @@ fn should_return_signer_port_cors_headers_for_home_parity() {
// 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_raw.contains("Access-Control-Allow-Origin: http://parity.web3.site"),
"CORS header for parity.web3.site missing: {:?}",
response.headers
);
}

View File

@ -31,7 +31,7 @@ fn should_resolve_dapp() {
let response = request(server,
"\
GET / HTTP/1.1\r\n\
Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.parity\r\n\
Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.web3.site\r\n\
Connection: close\r\n\
\r\n\
"
@ -52,7 +52,7 @@ fn should_return_503_when_syncing_but_should_make_the_calls() {
let response = request(server,
"\
GET / HTTP/1.1\r\n\
Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.parity\r\n\
Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.web3.site\r\n\
Connection: close\r\n\
\r\n\
"
@ -81,7 +81,7 @@ fn should_return_502_on_hash_mismatch() {
let response = request(server,
"\
GET / HTTP/1.1\r\n\
Host: 94f093625c06887d94d9fee0d5f9cc4aaa46f33d24d1c7e4b5237e7c37d547dd.parity\r\n\
Host: 94f093625c06887d94d9fee0d5f9cc4aaa46f33d24d1c7e4b5237e7c37d547dd.web3.site\r\n\
Connection: close\r\n\
\r\n\
"
@ -112,7 +112,7 @@ fn should_return_error_for_invalid_dapp_zip() {
let response = request(server,
"\
GET / HTTP/1.1\r\n\
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.web3.site\r\n\
Connection: close\r\n\
\r\n\
"
@ -144,7 +144,7 @@ fn should_return_fetched_dapp_content() {
let response1 = http_client::request(server.addr(),
"\
GET /index.html HTTP/1.1\r\n\
Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.parity\r\n\
Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.web3.site\r\n\
Connection: close\r\n\
\r\n\
"
@ -152,7 +152,7 @@ fn should_return_fetched_dapp_content() {
let response2 = http_client::request(server.addr(),
"\
GET /manifest.json HTTP/1.1\r\n\
Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.parity\r\n\
Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.web3.site\r\n\
Connection: close\r\n\
\r\n\
"
@ -207,7 +207,7 @@ fn should_return_fetched_content() {
let response = request(server,
"\
GET / HTTP/1.1\r\n\
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.web3.site\r\n\
Connection: close\r\n\
\r\n\
"
@ -234,7 +234,7 @@ fn should_cache_content() {
);
let request_str = "\
GET / HTTP/1.1\r\n\
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.web3.site\r\n\
Connection: close\r\n\
\r\n\
";
@ -265,7 +265,7 @@ fn should_not_request_content_twice() {
);
let request_str = "\
GET / HTTP/1.1\r\n\
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.web3.site\r\n\
Connection: close\r\n\
\r\n\
";
@ -298,6 +298,17 @@ fn should_not_request_content_twice() {
response2.assert_status("HTTP/1.1 200 OK");
}
#[test]
fn should_encode_and_decode_base32() {
use base32;
let encoded = base32::encode(base32::Alphabet::Crockford, "token+https://parity.io".as_bytes());
assert_eq!("EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY", &encoded);
let data = base32::decode(base32::Alphabet::Crockford, "EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY").unwrap();
assert_eq!("token+https://parity.io", &String::from_utf8(data).unwrap());
}
#[test]
fn should_stream_web_content() {
// given
@ -306,8 +317,8 @@ fn should_stream_web_content() {
// when
let response = request(server,
"\
GET /web/token/https/parity.io/ HTTP/1.1\r\n\
Host: localhost:8080\r\n\
GET / HTTP/1.1\r\n\
Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\
Connection: close\r\n\
\r\n\
"
@ -322,20 +333,90 @@ fn should_stream_web_content() {
}
#[test]
fn should_return_error_on_invalid_token() {
fn should_support_base32_encoded_web_urls() {
// given
let (server, fetch) = serve_with_fetch("token");
// when
let response = request(server,
"\
GET /web/invalidtoken/https/parity.io/ HTTP/1.1\r\n\
GET /styles.css?test=123 HTTP/1.1\r\n\
Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\
Connection: close\r\n\
\r\n\
"
);
// then
response.assert_status("HTTP/1.1 200 OK");
assert_security_headers_for_embed(&response.headers);
fetch.assert_requested("https://parity.io/styles.css?test=123");
fetch.assert_no_more_requests();
}
#[test]
fn should_correctly_handle_long_label_when_splitted() {
// given
let (server, fetch) = serve_with_fetch("xolrg9fePeQyKLnL");
// when
let response = request(server,
"\
GET /styles.css?test=123 HTTP/1.1\r\n\
Host: f1qprwk775k6am35a5wmpk3e9gnpgx3me1sk.mbsfcdqpwx3jd5h7ax39dxq2wvb5dhqpww3fe9t2wrvfdm.web.web3.site\r\n\
Connection: close\r\n\
\r\n\
"
);
// then
response.assert_status("HTTP/1.1 200 OK");
assert_security_headers_for_embed(&response.headers);
fetch.assert_requested("https://contribution.melonport.com/styles.css?test=123");
fetch.assert_no_more_requests();
}
#[test]
fn should_support_base32_encoded_web_urls_as_path() {
// given
let (server, fetch) = serve_with_fetch("token");
// when
let response = request(server,
"\
GET /web/EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY/styles.css?test=123 HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Connection: close\r\n\
\r\n\
"
);
// then
response.assert_status("HTTP/1.1 200 OK");
assert_security_headers_for_embed(&response.headers);
fetch.assert_requested("https://parity.io/styles.css?test=123");
fetch.assert_no_more_requests();
}
#[test]
fn should_return_error_on_invalid_token() {
// given
let (server, fetch) = serve_with_fetch("test");
// when
let response = request(server,
"\
GET / HTTP/1.1\r\n\
Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\
Connection: close\r\n\
\r\n\
"
);
// then
response.assert_status("HTTP/1.1 400 Bad Request");
assert_security_headers_for_embed(&response.headers);
@ -365,28 +446,6 @@ fn should_return_error_on_invalid_protocol() {
fetch.assert_no_more_requests();
}
#[test]
fn should_redirect_if_trailing_slash_is_missing() {
// given
let (server, fetch) = serve_with_fetch("token");
// when
let response = request(server,
"\
GET /web/token/https/parity.io HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Connection: close\r\n\
\r\n\
"
);
// then
response.assert_status("HTTP/1.1 302 Found");
response.assert_header("Location", "/web/token/https/parity.io/");
fetch.assert_no_more_requests();
}
#[test]
fn should_disallow_non_get_requests() {
// given
@ -395,8 +454,8 @@ fn should_disallow_non_get_requests() {
// when
let response = request(server,
"\
POST /token/https/parity.io/ HTTP/1.1\r\n\
Host: web.parity\r\n\
POST / HTTP/1.1\r\n\
Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\
Content-Type: application/json\r\n\
Connection: close\r\n\
\r\n\
@ -423,14 +482,37 @@ fn should_fix_absolute_requests_based_on_referer() {
GET /styles.css HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Connection: close\r\n\
Referer: http://localhost:8080/web/token/https/parity.io/\r\n\
Referer: http://localhost:8080/web/EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY/\r\n\
\r\n\
"
);
// then
response.assert_status("HTTP/1.1 302 Found");
response.assert_header("Location", "/web/token/https/parity.io/styles.css");
response.assert_header("Location", "/web/EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY/styles.css");
fetch.assert_no_more_requests();
}
#[test]
fn should_fix_absolute_requests_based_on_referer_in_url() {
// given
let (server, fetch) = serve_with_fetch("token");
// when
let response = request(server,
"\
GET /styles.css HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Connection: close\r\n\
Referer: http://localhost:8080/?__referer=web/EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY/\r\n\
\r\n\
"
);
// then
response.assert_status("HTTP/1.1 302 Found");
response.assert_header("Location", "/web/EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY/styles.css");
fetch.assert_no_more_requests();
}

View File

@ -105,7 +105,7 @@ fn should_display_404_on_invalid_dapp_with_domain() {
let response = request(server,
"\
GET / HTTP/1.1\r\n\
Host: invaliddapp.parity\r\n\
Host: invaliddapp.web3.site\r\n\
Connection: close\r\n\
\r\n\
"
@ -179,7 +179,7 @@ fn should_serve_proxy_pac() {
// then
assert_eq!(response.status, "HTTP/1.1 200 OK".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_eq!(response.body, "DD\n\nfunction FindProxyForURL(url, host) {\n\tif (shExpMatch(host, \"parity.web3.site\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:18180\";\n\t}\n\n\tif (shExpMatch(host, \"*.web3.site\"))\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);
}

View File

@ -23,7 +23,7 @@ use tests::helpers::{serve_with_rpc, request};
#[test]
fn should_serve_rpc() {
// given
let mut io = MetaIoHandler::new();
let mut io = MetaIoHandler::default();
io.add_method("rpc_test", |_| {
Ok(Value::String("Hello World!".into()))
});
@ -53,7 +53,7 @@ fn should_serve_rpc() {
#[test]
fn should_extract_metadata() {
// given
let mut io = MetaIoHandler::new();
let mut io = MetaIoHandler::default();
io.add_method_with_meta("rpc_test", |_params, meta: Metadata| {
assert_eq!(meta.dapp_id, Some("https://parity.io/".to_owned()));
assert_eq!(meta.origin, Origin::Dapps);
@ -87,7 +87,7 @@ fn should_extract_metadata() {
#[test]
fn should_extract_metadata_from_custom_header() {
// given
let mut io = MetaIoHandler::new();
let mut io = MetaIoHandler::default();
io.add_method_with_meta("rpc_test", |_params, meta: Metadata| {
assert_eq!(meta.dapp_id, Some("https://parity.io/".to_owned()));
assert_eq!(meta.origin, Origin::Dapps);

View File

@ -66,7 +66,7 @@ fn should_serve_dapps_domains() {
let response = request(server,
"\
GET / HTTP/1.1\r\n\
Host: ui.parity\r\n\
Host: ui.web3.site\r\n\
Connection: close\r\n\
\r\n\
{}

View File

@ -20,6 +20,7 @@ use std::sync::Arc;
use fetch::{self, Fetch};
use parity_reactor::Remote;
use base32;
use hyper::{self, server, net, Next, Encoder, Decoder};
use hyper::status::StatusCode;
@ -27,7 +28,7 @@ use apps;
use endpoint::{Endpoint, Handler, EndpointPath};
use handlers::{
ContentFetcherHandler, ContentHandler, ContentValidator, ValidatorResponse,
StreamingHandler, Redirection, extract_url,
StreamingHandler, extract_url,
};
use url::Url;
use WebProxyTokens;
@ -86,9 +87,10 @@ impl ContentValidator for WebInstaller {
);
if is_html {
handler.set_initial_content(&format!(
r#"<script src="/{}/inject.js"></script><script>history.replaceState({{}}, "", "/?{}{}")</script>"#,
r#"<script src="/{}/inject.js"></script><script>history.replaceState({{}}, "", "/?{}{}/{}")</script>"#,
apps::UTILS_PATH,
apps::URL_REFERER,
apps::WEB_PATH,
&self.referer,
));
}
@ -99,7 +101,6 @@ impl ContentValidator for WebInstaller {
enum State<F: Fetch> {
Initial,
Error(ContentHandler),
Redirecting(Redirection),
Fetching(ContentFetcherHandler<WebInstaller, F>),
}
@ -114,25 +115,26 @@ struct WebHandler<F: Fetch> {
}
impl<F: Fetch> WebHandler<F> {
fn extract_target_url(&self, url: Option<Url>) -> Result<(String, String), State<F>> {
let (path, query) = match url {
Some(url) => (url.path, url.query),
None => {
return Err(State::Error(ContentHandler::error(
StatusCode::BadRequest, "Invalid URL", "Couldn't parse URL", None, self.embeddable_on.clone()
)));
}
};
fn extract_target_url(&self, url: Option<Url>) -> Result<String, State<F>> {
let token_and_url = self.path.app_params.get(0)
.map(|encoded| encoded.replace('.', ""))
.and_then(|encoded| base32::decode(base32::Alphabet::Crockford, &encoded.to_uppercase()))
.and_then(|data| String::from_utf8(data).ok())
.ok_or_else(|| State::Error(ContentHandler::error(
StatusCode::BadRequest,
"Invalid parameter",
"Couldn't parse given parameter:",
self.path.app_params.get(0).map(String::as_str),
self.embeddable_on.clone()
)))?;
// Support domain based routing.
let idx = match path.get(0).map(|m| m.as_ref()) {
Some(apps::WEB_PATH) => 1,
_ => 0,
};
let mut token_it = token_and_url.split('+');
let token = token_it.next();
let target_url = token_it.next();
// Check if token supplied in URL is correct.
match path.get(idx) {
Some(ref token) if self.web_proxy_tokens.is_web_proxy_token_valid(token) => {},
match token {
Some(token) if self.web_proxy_tokens.is_web_proxy_token_valid(token) => {},
_ => {
return Err(State::Error(ContentHandler::error(
StatusCode::BadRequest, "Invalid Access Token", "Invalid or old web proxy access token supplied.", Some("Try refreshing the page."), self.embeddable_on.clone()
@ -141,9 +143,8 @@ impl<F: Fetch> WebHandler<F> {
}
// Validate protocol
let protocol = match path.get(idx + 1).map(|a| a.as_str()) {
Some("http") => "http",
Some("https") => "https",
let mut target_url = match target_url {
Some(url) if url.starts_with("http://") || url.starts_with("https://") => url.to_owned(),
_ => {
return Err(State::Error(ContentHandler::error(
StatusCode::BadRequest, "Invalid Protocol", "Invalid protocol used.", None, self.embeddable_on.clone()
@ -151,28 +152,35 @@ impl<F: Fetch> WebHandler<F> {
}
};
// Redirect if address to main page does not end with /
if let None = path.get(idx + 3) {
return Err(State::Redirecting(
Redirection::new(&format!("/{}/", path.join("/")))
));
if !target_url.ends_with("/") {
target_url = format!("{}/", target_url);
}
let query = match query {
Some(query) => format!("?{}", query),
// TODO [ToDr] Should just use `path.app_params`
let (path, query) = match (&url, self.path.using_dapps_domains) {
(&Some(ref url), true) => (&url.path[..], &url.query),
(&Some(ref url), false) => (&url.path[2..], &url.query),
_ => {
return Err(State::Error(ContentHandler::error(
StatusCode::BadRequest, "Invalid URL", "Couldn't parse URL", None, self.embeddable_on.clone()
)));
}
};
let query = match *query {
Some(ref query) => format!("?{}", query),
None => "".into(),
};
Ok((format!("{}://{}{}", protocol, path[idx + 2..].join("/"), query), path[0..].join("/")))
Ok(format!("{}{}{}", target_url, path.join("/"), query))
}
}
impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> {
fn on_request(&mut self, request: server::Request<net::HttpStream>) -> Next {
let url = extract_url(&request);
// First extract the URL (reject invalid URLs)
let (target_url, referer) = match self.extract_target_url(url) {
let target_url = match self.extract_target_url(url) {
Ok(url) => url,
Err(error) => {
self.state = error;
@ -186,7 +194,9 @@ impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> {
self.control.clone(),
WebInstaller {
embeddable_on: self.embeddable_on.clone(),
referer: referer,
referer: self.path.app_params.get(0)
.expect("`target_url` is valid; app_params is not empty;qed")
.to_owned(),
},
self.embeddable_on.clone(),
self.remote.clone(),
@ -202,7 +212,6 @@ impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> {
match self.state {
State::Initial => Next::end(),
State::Error(ref mut handler) => handler.on_request_readable(decoder),
State::Redirecting(ref mut handler) => handler.on_request_readable(decoder),
State::Fetching(ref mut handler) => handler.on_request_readable(decoder),
}
}
@ -211,7 +220,6 @@ impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> {
match self.state {
State::Initial => Next::end(),
State::Error(ref mut handler) => handler.on_response(res),
State::Redirecting(ref mut handler) => handler.on_response(res),
State::Fetching(ref mut handler) => handler.on_response(res),
}
}
@ -220,7 +228,6 @@ impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> {
match self.state {
State::Initial => Next::end(),
State::Error(ref mut handler) => handler.on_response_writable(encoder),
State::Redirecting(ref mut handler) => handler.on_response_writable(encoder),
State::Fetching(ref mut handler) => handler.on_response_writable(encoder),
}
}

View File

@ -87,7 +87,7 @@ pub fn request(address: &SocketAddr, request: &str) -> Response {
let _ = req.read_to_string(&mut response);
let mut lines = response.lines();
let status = lines.next().unwrap().to_owned();
let status = lines.next().expect("Expected a response").to_owned();
let headers_raw = read_block(&mut lines, false);
let headers = headers_raw.split('\n').map(|v| v.to_owned()).collect();
let body = read_block(&mut lines, true);

View File

@ -262,6 +262,18 @@ impl Client {
Ok(client)
}
/// Wakes up client if it's a sleep.
pub fn keep_alive(&self) {
let should_wake = match *self.mode.lock() {
Mode::Dark(..) | Mode::Passive(..) => true,
_ => false,
};
if should_wake {
self.wake_up();
(*self.sleep_state.lock()).last_activity = Some(Instant::now());
}
}
/// Adds an actor to be notified on certain events
pub fn add_notify(&self, target: Arc<ChainNotify>) {
self.notify.write().push(Arc::downgrade(&target));
@ -1011,17 +1023,6 @@ impl BlockChainClient for Client {
Ok(ret)
}
fn keep_alive(&self) {
let should_wake = match *self.mode.lock() {
Mode::Dark(..) | Mode::Passive(..) => true,
_ => false,
};
if should_wake {
self.wake_up();
(*self.sleep_state.lock()).last_activity = Some(Instant::now());
}
}
fn mode(&self) -> IpcMode {
let r = self.mode.lock().clone().into();
trace!(target: "mode", "Asked for mode = {:?}. returning {:?}", &*self.mode.lock(), r);

View File

@ -46,10 +46,6 @@ use encoded;
/// Blockchain database client. Owns and manages a blockchain and a block queue.
pub trait BlockChainClient : Sync + Send {
/// Should be called by any external-facing interface when actively using the client.
/// To minimise chatter, there's no need to call more than once every 30s.
fn keep_alive(&self) {}
/// Get raw block header data by block id.
fn block_header(&self, id: BlockId) -> Option<encoded::Header>;

View File

@ -1,6 +1,6 @@
{
"name": "parity.js",
"version": "0.3.62",
"version": "0.3.66",
"main": "release/index.js",
"jsnext:main": "src/index.js",
"author": "Parity Team <admin@parity.io>",
@ -140,6 +140,7 @@
"yargs": "6.6.0"
},
"dependencies": {
"base32.js": "0.1.0",
"bignumber.js": "3.0.1",
"blockies": "0.0.2",
"brace": "0.9.0",
@ -193,6 +194,7 @@
"scryptsy": "2.0.0",
"solc": "ngotchac/solc-js",
"store": "1.3.20",
"useragent.js": "0.5.6",
"utf8": "2.1.2",
"valid-url": "1.0.9",
"validator": "6.2.0",

View File

@ -127,6 +127,18 @@ export function inNumber16 (number) {
return inHex(bn.toString(16));
}
export function inOptionsCondition (condition) {
if (condition) {
if (condition.block) {
condition.block = condition.block ? inNumber10(condition.block) : null;
} else if (condition.time) {
condition.time = inNumber10(Math.floor(condition.time.getTime() / 1000));
}
}
return condition;
}
export function inOptions (options) {
if (options) {
Object.keys(options).forEach((key) => {
@ -136,6 +148,10 @@ export function inOptions (options) {
options[key] = inAddress(options[key]);
break;
case 'condition':
options[key] = inOptionsCondition(options[key]);
break;
case 'gas':
case 'gasPrice':
options[key] = inNumber16((new BigNumber(options[key])).round());

View File

@ -221,6 +221,18 @@ export function outSyncing (syncing) {
return syncing;
}
export function outTransactionCondition (condition) {
if (condition) {
if (condition.block) {
condition.block = outNumber(condition.block);
} else if (condition.time) {
condition.time = outDate(condition.time);
}
}
return condition;
}
export function outTransaction (tx) {
if (tx) {
Object.keys(tx).forEach((key) => {
@ -234,8 +246,14 @@ export function outTransaction (tx) {
tx[key] = outNumber(tx[key]);
break;
case 'condition':
tx[key] = outTransactionCondition(tx[key]);
break;
case 'minBlock':
tx[key] = tx[key] ? outNumber(tx[key]) : null;
tx[key] = tx[key]
? outNumber(tx[key])
: null;
break;
case 'creates':

View File

@ -20,7 +20,6 @@
}
.container {
margin-top: 1.5em;
overflow-y: auto;
}

View File

@ -18,7 +18,7 @@ import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { ContainerTitle, DappCard, Portal, SectionList } from '~/ui';
import { DappCard, Portal, SectionList } from '~/ui';
import { CheckIcon } from '~/ui/Icons';
import styles from './addDapps.css';
@ -41,15 +41,13 @@ export default class AddDapps extends Component {
className={ styles.modal }
onClose={ store.closeModal }
open
title={
<FormattedMessage
id='dapps.add.label'
defaultMessage='visible applications'
/>
}
>
<ContainerTitle
title={
<FormattedMessage
id='dapps.add.label'
defaultMessage='visible applications'
/>
}
/>
<div className={ styles.container }>
<div className={ styles.warning } />
{

View File

@ -15,12 +15,7 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.modal {
flex-direction: column;
}
.container {
margin-top: 1.5em;
overflow-y: auto;
}
@ -65,7 +60,6 @@
.legend {
opacity: 0.75;
margin-top: 1em;
span {
line-height: 24px;

View File

@ -18,7 +18,7 @@ import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { AccountCard, ContainerTitle, Portal, SectionList } from '~/ui';
import { AccountCard, Portal, SectionList } from '~/ui';
import { CheckIcon, StarIcon, StarOutlineIcon } from '~/ui/Icons';
import styles from './dappPermissions.css';
@ -38,18 +38,27 @@ export default class DappPermissions extends Component {
return (
<Portal
className={ styles.modal }
buttons={
<div className={ styles.legend }>
<FormattedMessage
id='dapps.permissions.description'
defaultMessage='{activeIcon} account is available to application, {defaultIcon} account is the default account'
values={ {
activeIcon: <CheckIcon />,
defaultIcon: <StarIcon />
} }
/>
</div>
}
onClose={ store.closeModal }
open
title={
<FormattedMessage
id='dapps.permissions.label'
defaultMessage='visible dapp accounts'
/>
}
>
<ContainerTitle
title={
<FormattedMessage
id='dapps.permissions.label'
defaultMessage='visible dapp accounts'
/>
}
/>
<div className={ styles.container }>
<SectionList
items={ store.accounts }
@ -57,16 +66,6 @@ export default class DappPermissions extends Component {
renderItem={ this.renderAccount }
/>
</div>
<div className={ styles.legend }>
<FormattedMessage
id='dapps.permissions.description'
defaultMessage='{activeIcon} account is available to application, {defaultIcon} account is the default account'
values={ {
activeIcon: <CheckIcon />,
defaultIcon: <StarIcon />
} }
/>
</div>
</Portal>
);
}

View File

@ -15,45 +15,22 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Input, GasPriceEditor } from '~/ui';
import { GasPriceEditor } from '~/ui';
import styles from '../executeContract.css';
export default class AdvancedStep extends Component {
static propTypes = {
gasStore: PropTypes.object.isRequired,
minBlock: PropTypes.string,
minBlockError: PropTypes.string,
onMinBlockChange: PropTypes.func
gasStore: PropTypes.object.isRequired
};
render () {
const { gasStore, minBlock, minBlockError, onMinBlockChange } = this.props;
const { gasStore } = this.props;
return (
<div>
<Input
error={ minBlockError }
hint={
<FormattedMessage
id='executeContract.advanced.minBlock.hint'
defaultMessage='Only post the transaction after this block'
/>
}
label={
<FormattedMessage
id='executeContract.advanced.minBlock.label'
defaultMessage='BlockNumber to send from'
/>
}
value={ minBlock }
onSubmit={ onMinBlockChange }
/>
<div className={ styles.gaseditor }>
<GasPriceEditor store={ gasStore } />
</div>
<div className={ styles.gaseditor }>
<GasPriceEditor store={ gasStore } />
</div>
);
}

View File

@ -14,7 +14,6 @@
// 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 { pick } from 'lodash';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
@ -100,8 +99,6 @@ class ExecuteContract extends Component {
fromAddressError: null,
func: null,
funcError: null,
minBlock: '0',
minBlockError: null,
rejected: false,
sending: false,
step: STEP_DETAILS,
@ -167,8 +164,8 @@ class ExecuteContract extends Component {
renderDialogActions () {
const { onClose, fromAddress } = this.props;
const { advancedOptions, sending, step, fromAddressError, minBlockError, valuesError } = this.state;
const hasError = fromAddressError || minBlockError || valuesError.find((error) => error);
const { advancedOptions, sending, step, fromAddressError, valuesError } = this.state;
const hasError = fromAddressError || valuesError.find((error) => error);
const cancelBtn = (
<Button
@ -258,7 +255,7 @@ class ExecuteContract extends Component {
renderStep () {
const { onFromAddressChange } = this.props;
const { advancedOptions, step, busyState, minBlock, minBlockError, txhash, rejected } = this.state;
const { advancedOptions, step, busyState, txhash, rejected } = this.state;
if (rejected) {
return (
@ -305,12 +302,7 @@ class ExecuteContract extends Component {
);
} else if (advancedOptions && (step === STEP_BUSY_OR_ADVANCED)) {
return (
<AdvancedStep
gasStore={ this.gasStore }
minBlock={ minBlock }
minBlockError={ minBlockError }
onMinBlockChange={ this.onMinBlockChange }
/>
<AdvancedStep gasStore={ this.gasStore } />
);
}
@ -339,15 +331,6 @@ class ExecuteContract extends Component {
}, this.estimateGas);
}
onMinBlockChange = (minBlock) => {
const minBlockError = validateUint(minBlock).valueError;
this.setState({
minBlock,
minBlockError
});
}
onValueChange = (event, index, _value) => {
const { func, values, valuesError } = this.state;
const input = func.inputs.find((input, _index) => index === _index);
@ -409,17 +392,14 @@ class ExecuteContract extends Component {
postTransaction = () => {
const { api, store } = this.context;
const { fromAddress } = this.props;
const { advancedOptions, amount, func, minBlock, values } = this.state;
const { advancedOptions, amount, func, values } = this.state;
const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC;
const finalstep = steps.length - 1;
const options = {
gas: this.gasStore.gas,
gasPrice: this.gasStore.price,
const options = this.gasStore.overrideTransaction({
from: fromAddress,
minBlock: new BigNumber(minBlock || 0).gt(0) ? minBlock : null,
value: api.util.toWei(amount || 0)
};
});
this.setState({ sending: true, step: advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED });

View File

@ -27,36 +27,17 @@ export default class Extras extends Component {
dataError: PropTypes.string,
gasStore: PropTypes.object.isRequired,
isEth: PropTypes.bool,
minBlock: PropTypes.string,
minBlockError: PropTypes.string,
onChange: PropTypes.func.isRequired,
total: PropTypes.string,
totalError: PropTypes.string
}
render () {
const { gasStore, minBlock, minBlockError, onChange } = this.props;
const { gasStore, onChange } = this.props;
return (
<Form>
{ this.renderData() }
<Input
error={ minBlockError }
hint={
<FormattedMessage
id='transferModal.minBlock.hint'
defaultMessage='Only post the transaction after this block'
/>
}
label={
<FormattedMessage
id='transferModal.minBlock.label'
defaultMessage='BlockNumber to send from'
/>
}
value={ minBlock }
onChange={ this.onEditMinBlock }
/>
<div className={ styles.gaseditor }>
<GasPriceEditor
store={ gasStore }
@ -98,8 +79,4 @@ export default class Extras extends Component {
onEditData = (event) => {
this.props.onChange('data', event.target.value);
}
onEditMinBlock = (event) => {
this.props.onChange('minBlock', event.target.value);
}
}

View File

@ -52,9 +52,6 @@ export default class TransferStore {
@observable data = '';
@observable dataError = null;
@observable minBlock = '0';
@observable minBlockError = null;
@observable recipient = '';
@observable recipientError = ERRORS.requireRecipient;
@ -78,39 +75,6 @@ export default class TransferStore {
gasStore = null;
@computed get steps () {
const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC);
if (this.rejected) {
steps[steps.length - 1] = TITLES.rejected;
}
return steps;
}
@computed get isValid () {
const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError;
const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.minBlockError && !this.totalError;
const verifyValid = !this.passwordError;
switch (this.stage) {
case 0:
return detailsValid;
case 1:
return this.extras
? extrasValid
: verifyValid;
case 2:
return verifyValid;
}
}
get token () {
return this.balance.tokens.find((balance) => balance.token.tag === this.tag).token;
}
constructor (api, props) {
this.api = api;
@ -135,6 +99,39 @@ export default class TransferStore {
}
}
@computed get steps () {
const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC);
if (this.rejected) {
steps[steps.length - 1] = TITLES.rejected;
}
return steps;
}
@computed get isValid () {
const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError;
const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.gasStore.conditionBlockError && !this.totalError;
const verifyValid = !this.passwordError;
switch (this.stage) {
case 0:
return detailsValid;
case 1:
return this.extras
? extrasValid
: verifyValid;
case 2:
return verifyValid;
}
}
get token () {
return this.balance.tokens.find((balance) => balance.token.tag === this.tag).token;
}
@action onNext = () => {
this.stage += 1;
}
@ -164,9 +161,6 @@ export default class TransferStore {
case 'gasPrice':
return this._onUpdateGasPrice(value);
case 'minBlock':
return this._onUpdateMinBlock(value);
case 'recipient':
return this._onUpdateRecipient(value);
@ -284,14 +278,6 @@ export default class TransferStore {
this.recalculate();
}
@action _onUpdateMinBlock = (minBlock) => {
console.log('minBlock', minBlock);
transaction(() => {
this.minBlock = minBlock;
this.minBlockError = this._validatePositiveNumber(minBlock);
});
}
@action _onUpdateGasPrice = (gasPrice) => {
this.recalculate();
}
@ -590,7 +576,6 @@ export default class TransferStore {
send () {
const { options, values } = this._getTransferParams();
options.minBlock = new BigNumber(this.minBlock || 0).gt(0) ? this.minBlock : null;
log.debug('@send', 'transfer value', options.value && options.value.toFormat());
return this._getTransferMethod().postTransaction(options, values);
@ -639,15 +624,12 @@ export default class TransferStore {
const to = (isEth && !isWallet) ? this.recipient
: (this.isWallet ? this.wallet.address : this.token.address);
const options = {
const options = this.gasStore.overrideTransaction({
from: this.sender || this.account.address,
to
};
});
if (!gas) {
options.gas = this.gasStore.gas;
options.gasPrice = this.gasStore.price;
} else {
if (gas) {
options.gas = MAX_GAS_ESTIMATION;
}

View File

@ -207,7 +207,7 @@ class Transfer extends Component {
return null;
}
const { isEth, data, dataError, minBlock, minBlockError, total, totalError } = this.store;
const { isEth, data, dataError, total, totalError } = this.store;
return (
<Extras
@ -215,8 +215,6 @@ class Transfer extends Component {
dataError={ dataError }
gasStore={ this.store.gasStore }
isEth={ isEth }
minBlock={ minBlock }
minBlockError={ minBlockError }
onChange={ this.store.onUpdateDetails }
total={ total }
totalError={ totalError }

View File

@ -52,13 +52,11 @@ export default class SignerMiddleware {
}
onConfirmStart = (store, action) => {
const { gas = 0, gasPrice = 0, id, password, payload, wallet } = action.payload;
const { condition, gas = 0, gasPrice = 0, id, password, payload, wallet } = action.payload;
const handlePromise = (promise) => {
promise
.then((txHash) => {
console.log('confirmRequest', id, txHash);
if (!txHash) {
store.dispatch(actions.errorConfirmRequest({ id, err: 'Unable to confirm.' }));
return;
@ -120,7 +118,7 @@ export default class SignerMiddleware {
});
}
handlePromise(this._api.signer.confirmRequest(id, { gas, gasPrice }, password));
handlePromise(this._api.signer.confirmRequest(id, { gas, gasPrice, condition }, password));
}
onRejectStart = (store, action) => {

View File

@ -14,30 +14,36 @@
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.byline, .description {
$bylineColor: #aaa;
$bylineLineHeight: 1.2rem;
$bylineMaxHeight: 2.4rem;
$titleLineHeight: 2rem;
$smallFontSize: 0.75rem;
.byline,
.description {
color: $bylineColor;
display: -webkit-box;
line-height: $bylineLineHeight;
max-height: $bylineMaxHeight;
overflow: hidden;
position: relative;
line-height: 1.2em;
max-height: 2.4em;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
color: #aaa;
* {
color: #aaa !important;
color: $bylineColor !important;
}
}
.description {
font-size: 0.75em;
font-size: $smallFontSize;
margin: 0.5em 0 0;
}
.title {
text-transform: uppercase;
line-height: $titleLineHeight;
margin: 0;
line-height: 34px;
text-transform: uppercase;
}

View File

@ -29,29 +29,41 @@ export default class Title extends Component {
}
render () {
const { byline, className, title } = this.props;
const byLine = typeof byline === 'string'
? (
<span title={ byline }>
{ byline }
</span>
)
: byline;
const { className, title } = this.props;
return (
<div className={ className }>
<h3 className={ styles.title }>
{ title }
</h3>
<div className={ styles.byline }>
{ byLine }
</div>
{ this.renderByline() }
{ this.renderDescription() }
</div>
);
}
renderByline () {
const { byline } = this.props;
if (!byline) {
return null;
}
return (
<div className={ styles.byline }>
{
typeof byline === 'string'
? (
<span title={ byline }>
{ byline }
</span>
)
: byline
}
</div>
);
}
renderDescription () {
const { description } = this.props;
@ -59,17 +71,17 @@ export default class Title extends Component {
return null;
}
const desc = typeof description === 'string'
? (
<span title={ description }>
{ description }
</span>
)
: description;
return (
<div className={ styles.description }>
{ desc }
{
typeof description === 'string'
? (
<span title={ description }>
{ description }
</span>
)
: description
}
</div>
);
}

View File

@ -73,6 +73,12 @@
}
}
.title {
display: flex;
flex-direction: column;
position: relative;
}
.label {
margin: 1rem 0.5rem 0.25em;
color: rgba(255, 255, 255, 0.498039);
@ -102,14 +108,11 @@
}
.categories {
flex: 1;
display: flex;
flex: 1;
flex-direction: row;
justify-content: flex-start;
margin: 2rem 0 0;
> * {
flex: 1;
}

View File

@ -180,34 +180,38 @@ class AddressSelect extends Component {
onClose={ this.handleClose }
onKeyDown={ this.handleKeyDown }
open={ expanded }
title={
<div className={ styles.title }>
<label className={ styles.label } htmlFor={ id }>
{ label }
</label>
<div className={ styles.outerInput }>
<input
id={ id }
className={ styles.input }
placeholder={ ilHint }
onBlur={ this.handleInputBlur }
onFocus={ this.handleInputFocus }
onChange={ this.handleChange }
ref={ this.setInputRef }
/>
{ this.renderLoader() }
</div>
<div className={ styles.underline }>
<TextFieldUnderline
focus={ inputFocused }
focusStyle={ BOTTOM_BORDER_STYLE }
muiTheme={ muiTheme }
style={ BOTTOM_BORDER_STYLE }
/>
</div>
{ this.renderCurrentInput() }
{ this.renderRegistryValues() }
</div>
}
>
<label className={ styles.label } htmlFor={ id }>
{ label }
</label>
<div className={ styles.outerInput }>
<input
id={ id }
className={ styles.input }
placeholder={ ilHint }
onBlur={ this.handleInputBlur }
onFocus={ this.handleInputFocus }
onChange={ this.handleChange }
ref={ this.setInputRef }
/>
{ this.renderLoader() }
</div>
<div className={ styles.underline }>
<TextFieldUnderline
focus={ inputFocused }
focusStyle={ BOTTOM_BORDER_STYLE }
muiTheme={ muiTheme }
style={ BOTTOM_BORDER_STYLE }
/>
</div>
{ this.renderCurrentInput() }
{ this.renderRegistryValues() }
{ this.renderAccounts() }
</Portal>
);

View File

@ -0,0 +1,61 @@
// Copyright 2015-2017 Parity Technologies (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 keycode from 'keycode';
import React, { Component, PropTypes } from 'react';
export default class DappUrlInput extends Component {
static propTypes = {
className: PropTypes.string,
onChange: PropTypes.func.isRequired,
onGoto: PropTypes.func.isRequired,
onRestore: PropTypes.func.isRequired,
url: PropTypes.string.isRequired
}
render () {
const { className, url } = this.props;
return (
<input
className={ className }
onChange={ this.onChange }
onKeyDown={ this.onKeyDown }
type='text'
value={ url }
/>
);
}
onChange = (event) => {
this.props.onChange(event.target.value);
};
onKeyDown = (event) => {
switch (keycode(event)) {
case 'esc':
this.props.onRestore();
break;
case 'enter':
this.props.onGoto();
break;
default:
break;
}
};
}

View File

@ -0,0 +1,70 @@
// Copyright 2015-2017 Parity Technologies (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 { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import DappUrlInput from './';
let component;
let onChange;
let onGoto;
let onRestore;
function render (props = { url: 'http://some.url' }) {
onChange = sinon.stub();
onGoto = sinon.stub();
onRestore = sinon.stub();
component = shallow(
<DappUrlInput
onChange={ onChange }
onGoto={ onGoto }
onRestore={ onRestore }
{ ...props }
/>
);
return component;
}
describe('ui/Form/DappUrlInput', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
describe('events', () => {
describe('onChange', () => {
it('calls the onChange callback as provided', () => {
component.simulate('change', { target: { value: 'testing' } });
expect(onChange).to.have.been.calledWith('testing');
});
});
describe('onKeyDown', () => {
it('calls the onGoto callback on enter', () => {
component.simulate('keyDown', { keyCode: 13 });
expect(onGoto).to.have.been.called;
});
it('calls the onRestor callback on esc', () => {
component.simulate('keyDown', { keyCode: 27 });
expect(onRestore).to.have.been.called;
});
});
});
});

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (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 './dappUrlInput';

View File

@ -78,6 +78,9 @@ class InputAddress extends Component {
props.focused = focused;
}
// FIXME: The is not advisable, fixes the display issue, however the name should come from
// a common component.
// account.name || (value ? 'UNNAMED' : value)
return (
<div className={ containerClasses.join(' ') }>
<Input
@ -96,7 +99,7 @@ class InputAddress extends Component {
tabIndex={ tabIndex }
value={
text && account
? account.name
? (account.name || (value ? 'UNNAMED' : value))
: (nullName || value)
}
{ ...props }

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (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 './inputDate';

View File

@ -0,0 +1,22 @@
/* Copyright 2015-2017 Parity Technologies (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/>.
*/
.container {
.input {
width: 100%;
}
}

View File

@ -0,0 +1,53 @@
// Copyright 2015-2017 Parity Technologies (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 { DatePicker } from 'material-ui';
import React, { Component, PropTypes } from 'react';
import Label from '../Label';
import styles from './inputDate.css';
// NOTE: Has to be larger than Signer overlay Z, aligns with ../InputTime
const DIALOG_STYLE = { zIndex: 10010 };
export default class InputDate extends Component {
static propTypes = {
className: PropTypes.string,
hint: PropTypes.node,
label: PropTypes.node,
onChange: PropTypes.func,
value: PropTypes.object.isRequired
};
render () {
const { className, hint, label, onChange, value } = this.props;
return (
<div className={ [styles.container, className].join(' ') }>
<Label label={ label } />
<DatePicker
autoOk
className={ styles.input }
dialogContainerStyle={ DIALOG_STYLE }
hintText={ hint }
onChange={ onChange }
value={ value }
/>
</div>
);
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (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 './inputTime';

View File

@ -0,0 +1,22 @@
/* Copyright 2015-2017 Parity Technologies (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/>.
*/
.container {
.input {
width: 100%;
}
}

View File

@ -0,0 +1,54 @@
// Copyright 2015-2017 Parity Technologies (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 { TimePicker } from 'material-ui';
import React, { Component, PropTypes } from 'react';
import Label from '../Label';
import styles from './inputTime.css';
// NOTE: Has to be larger than Signer overlay Z, aligns with ../InputDate
const DIALOG_STYLE = { zIndex: 10010 };
export default class InputTime extends Component {
static propTypes = {
className: PropTypes.string,
hint: PropTypes.node,
label: PropTypes.node,
onChange: PropTypes.func,
value: PropTypes.object.isRequired
}
render () {
const { className, hint, label, onChange, value } = this.props;
return (
<div className={ [styles.container, className].join(' ') }>
<Label label={ label } />
<TimePicker
autoOk
className={ styles.input }
dialogStyle={ DIALOG_STYLE }
format='24hr'
hintText={ hint }
onChange={ onChange }
value={ value }
/>
</div>
);
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (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 './label';

View File

@ -0,0 +1,24 @@
/* Copyright 2015-2017 Parity Technologies (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/>.
*/
$labelColor: rgba(255, 255, 255, 0.5);
$labelFontSize: 0.75rem;
.label {
color: $labelColor;
font-size: $labelFontSize;
}

View File

@ -0,0 +1,40 @@
// Copyright 2015-2017 Parity Technologies (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 styles from './label.css';
export default class Label extends Component {
static propTypes = {
className: PropTypes.string,
label: PropTypes.node
}
render () {
const { className, label } = this.props;
if (!label) {
return null;
}
return (
<label className={ [styles.label, className].join(' ') }>
{ label }
</label>
);
}
}

View File

@ -15,18 +15,23 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.spaced {
margin: 0.25em 0;
}
.container {
.label {
}
.typeContainer {
display: flex;
flex-direction: column;
.radioButton {
margin: 0.25em 0;
}
.desc {
font-size: 0.8em;
margin-bottom: 0.5em;
color: #ccc;
z-index: 2;
.radioLabel {
display: flex;
flex-direction: column;
.description {
font-size: 0.8em;
margin-bottom: 0.5em;
color: #ccc;
z-index: 2;
}
}
}

View File

@ -18,10 +18,14 @@ import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
import React, { Component, PropTypes } from 'react';
import { arrayOrObjectProptype } from '~/util/proptypes';
import Label from '../Label';
import styles from './radioButtons.css';
export default class RadioButtons extends Component {
static propTypes = {
className: PropTypes.string,
label: PropTypes.node,
name: PropTypes.string,
onChange: PropTypes.func.isRequired,
value: PropTypes.any,
@ -34,10 +38,10 @@ export default class RadioButtons extends Component {
};
render () {
const { value, values } = this.props;
const { className, label, value, values } = this.props;
const index = Number.isNaN(parseInt(value))
? values.findIndex((val) => val.key === value)
? values.findIndex((_value) => _value.key === value)
: parseInt(value);
const selectedValue = typeof value !== 'object'
? values[index]
@ -45,13 +49,19 @@ export default class RadioButtons extends Component {
const key = this.getKey(selectedValue, index);
return (
<RadioButtonGroup
name={ name }
onChange={ this.onChange }
valueSelected={ key }
>
{ this.renderContent() }
</RadioButtonGroup>
<div className={ [styles.container, className].join(' ') }>
<Label
className={ styles.label }
label={ label }
/>
<RadioButtonGroup
name={ name }
onChange={ this.onChange }
valueSelected={ key }
>
{ this.renderContent() }
</RadioButtonGroup>
</div>
);
}
@ -67,14 +77,14 @@ export default class RadioButtons extends Component {
return (
<RadioButton
className={ styles.spaced }
className={ styles.radioButton }
key={ index }
label={
<div className={ styles.typeContainer }>
<div className={ styles.radioLabel }>
<span>{ label }</span>
{
description
? <span className={ styles.desc }>{ description }</span>
? <span className={ styles.description }>{ description }</span>
: null
}
</div>
@ -97,7 +107,7 @@ export default class RadioButtons extends Component {
onChange = (event, index) => {
const { onChange, values } = this.props;
const value = values[index] || values.find((v) => v.key === index);
const value = values[index] || values.find((value) => value.key === index);
onChange(value, index);
}

View File

@ -15,26 +15,34 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import AddressSelect from './AddressSelect';
import DappUrlInput from './DappUrlInput';
import FormWrap from './FormWrap';
import TypedInput from './TypedInput';
import Input from './Input';
import InputAddress from './InputAddress';
import InputAddressSelect from './InputAddressSelect';
import InputChip from './InputChip';
import InputDate from './InputDate';
import InputInline from './InputInline';
import Select from './Select';
import InputTime from './InputTime';
import Label from './Label';
import RadioButtons from './RadioButtons';
import Select from './Select';
import TypedInput from './TypedInput';
export default from './form';
export {
AddressSelect,
DappUrlInput,
FormWrap,
TypedInput,
Input,
InputAddress,
InputAddressSelect,
InputChip,
InputDate,
InputInline,
InputTime,
Label,
RadioButtons,
Select,
RadioButtons
TypedInput
};

View File

@ -16,6 +16,46 @@
*/
.container {
display: flex;
flex-direction: column;
}
.conditionContainer {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
margin-bottom: 1.5em;
.input {
flex: 0 1 50%;
}
}
.conditionRadio {
display: flex;
flex-direction: column;
margin-bottom: 1em;
&>label {
margin-bottom: 0.5em;
}
&>div {
display: flex;
flex-direction: row;
&>div {
width: auto !important;
label {
padding-right: 1.5em;
white-space: nowrap;
}
}
}
}
.graphContainer {
display: flex;
flex-wrap: wrap;
position: relative;

View File

@ -17,13 +17,44 @@
import BigNumber from 'bignumber.js';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import Input from '../Form/Input';
import { Input, InputDate, InputTime, RadioButtons } from '../Form';
import GasPriceSelector from '../GasPriceSelector';
import Store from './store';
import Store, { CONDITIONS } from './store';
import styles from './gasPriceEditor.css';
const CONDITION_VALUES = [
{
label: (
<FormattedMessage
id='txEditor.condition.none'
defaultMessage='No conditions'
/>
),
key: CONDITIONS.NONE
},
{
label: (
<FormattedMessage
id='txEditor.condition.blocknumber'
defaultMessage='Send after BlockNumber'
/>
),
key: CONDITIONS.BLOCK
},
{
label: (
<FormattedMessage
id='txEditor.condition.datetime'
defaultMessage='Send after Date & Time'
/>
),
key: CONDITIONS.TIME
}
];
@observer
export default class GasPriceEditor extends Component {
static contextTypes = {
@ -41,7 +72,7 @@ export default class GasPriceEditor extends Component {
render () {
const { api } = this.context;
const { children, store } = this.props;
const { errorGas, errorPrice, errorTotal, estimated, gas, histogram, price, priceDefault, totalValue } = store;
const { conditionType, errorGas, errorPrice, errorTotal, estimated, gas, histogram, price, priceDefault, totalValue } = store;
const eth = api.util.fromWei(totalValue).toFormat();
const gasLabel = `gas (estimated: ${new BigNumber(estimated).toFormat()})`;
@ -49,46 +80,146 @@ export default class GasPriceEditor extends Component {
return (
<div className={ styles.container }>
<div className={ styles.graphColumn }>
<GasPriceSelector
histogram={ histogram }
onChange={ this.onEditGasPrice }
price={ price }
/>
<div className={ styles.gasPriceDesc }>
You can choose the gas price based on the distribution of recent included transaction gas prices. The lower the gas price is, the cheaper the transaction will be. The higher the gas price is, the faster it should get mined by the network.
<RadioButtons
className={ styles.conditionRadio }
label={
<FormattedMessage
id='txEditor.condition.label'
defaultMessage='Condition where transaction activates'
/>
}
onChange={ this.onChangeConditionType }
value={ conditionType }
values={ CONDITION_VALUES }
/>
{ this.renderConditions() }
<div className={ styles.graphContainer }>
<div className={ styles.graphColumn }>
<GasPriceSelector
histogram={ histogram }
onChange={ this.onEditGasPrice }
price={ price }
/>
<div className={ styles.gasPriceDesc }>
<FormattedMessage
id='txEditor.gas.info'
defaultMessage='You can choose the gas price based on the distribution of recent included transaction gas prices. The lower the gas price is, the cheaper the transaction will be. The higher the gas price is, the faster it should get mined by the network.'
/>
</div>
</div>
<div className={ styles.editColumn }>
<div className={ styles.row }>
<Input
error={ errorGas }
hint='the amount of gas to use for the transaction'
label={ gasLabel }
min={ 1 }
onChange={ this.onEditGas }
type='number'
value={ gas }
/>
<Input
error={ errorPrice }
hint='the price of gas to use for the transaction'
label={ priceLabel }
min={ 1 }
onChange={ this.onEditGasPrice }
type='number'
value={ price }
/>
</div>
<div className={ styles.row }>
<Input
disabled
error={ errorTotal }
hint='the total amount of the transaction'
label='total transaction amount'
value={ `${eth} ETH` }
/>
</div>
<div className={ styles.row }>
{ children }
</div>
</div>
</div>
</div>
);
}
<div className={ styles.editColumn }>
<div className={ styles.row }>
renderConditions () {
const { conditionType, condition, conditionBlockError } = this.props.store;
if (conditionType === CONDITIONS.NONE) {
return null;
}
if (conditionType === CONDITIONS.BLOCK) {
return (
<div className={ styles.conditionContainer }>
<div className={ styles.input }>
<Input
error={ errorGas }
hint='the amount of gas to use for the transaction'
label={ gasLabel }
onChange={ this.onEditGas }
value={ gas }
/>
<Input
error={ errorPrice }
hint='the price of gas to use for the transaction'
label={ priceLabel }
onChange={ this.onEditGasPrice }
value={ price }
error={ conditionBlockError }
hint={
<FormattedMessage
id='txEditor.condition.block.hint'
defaultMessage='The minimum block to send from'
/>
}
label={
<FormattedMessage
id='txEditor.condition.block.label'
defaultMessage='Transaction send block'
/>
}
min={ 1 }
onChange={ this.onChangeConditionBlock }
type='number'
value={ condition.block }
/>
</div>
<div className={ styles.row }>
<Input
disabled
error={ errorTotal }
hint='the total amount of the transaction'
label='total transaction amount'
value={ `${eth} ETH` }
/>
</div>
<div className={ styles.row }>
{ children }
</div>
</div>
);
}
return (
<div className={ styles.conditionContainer }>
<div className={ styles.input }>
<InputDate
hint={
<FormattedMessage
id='txEditor.condition.date.hint'
defaultMessage='The minimum date to send from'
/>
}
label={
<FormattedMessage
id='txEditor.condition.date.label'
defaultMessage='Transaction send date'
/>
}
onChange={ this.onChangeConditionDateTime }
value={ condition.time }
/>
</div>
<div className={ styles.input }>
<InputTime
hint={
<FormattedMessage
id='txEditor.condition.time.hint'
defaultMessage='The minimum time to send from'
/>
}
label={
<FormattedMessage
id='txEditor.condition.time.label'
defaultMessage='Transaction send time'
/>
}
onChange={ this.onChangeConditionDateTime }
value={ condition.time }
/>
</div>
</div>
);
@ -107,4 +238,16 @@ export default class GasPriceEditor extends Component {
store.setPrice(price);
onChange && onChange('gasPrice', price);
}
onChangeConditionType = (conditionType) => {
this.props.store.setConditionType(conditionType.key);
}
onChangeConditionBlock = (event, blockNumber) => {
this.props.store.setConditionBlockNumber(blockNumber);
}
onChangeConditionDateTime = (event, datetime) => {
this.props.store.setConditionDateTime(datetime);
}
}

View File

@ -21,26 +21,64 @@ import sinon from 'sinon';
import GasPriceEditor from './';
const api = {
util: {
fromWei: (value) => new BigNumber(value)
}
};
let api;
let component;
let store;
const store = {
estimated: '123',
histogram: {},
priceDefault: '456',
totalValue: '789',
setGas: sinon.stub(),
setPrice: sinon.stub()
};
function createApi () {
api = {
eth: {
blockNumber: sinon.stub().resolves(new BigNumber(3))
},
util: {
fromWei: (value) => new BigNumber(value)
}
};
return api;
}
function createStore () {
createApi();
store = {
_api: api,
conditionType: 'none',
estimated: '123',
histogram: {},
priceDefault: '456',
totalValue: '789',
setGas: sinon.stub(),
setPrice: sinon.stub()
};
return store;
}
function render (props = {}) {
createStore();
component = shallow(
<GasPriceEditor
store={ store }
{ ...props }
/>,
{
context: {
api
}
}
);
return component;
}
describe('ui/GasPriceEditor', () => {
beforeEach(() => {
render();
});
it('renders', () => {
expect(shallow(
<GasPriceEditor store={ store } />,
{ context: { api } }
)).to.be.ok;
expect(component).to.be.ok;
});
});

View File

@ -20,7 +20,17 @@ import { action, computed, observable, transaction } from 'mobx';
import { ERRORS, validatePositiveNumber } from '~/util/validation';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants';
const CONDITIONS = {
NONE: 'none',
BLOCK: 'blockNumber',
TIME: 'timestamp'
};
export default class GasPriceEditor {
@observable blockNumber = 0;
@observable condition = {};
@observable conditionBlockError = null;
@observable conditionType = CONDITIONS.NONE;
@observable errorEstimated = null;
@observable errorGas = null;
@observable errorPrice = null;
@ -34,13 +44,23 @@ export default class GasPriceEditor {
@observable priceDefault;
@observable weiValue = '0';
constructor (api, { gas, gasLimit, gasPrice }) {
constructor (api, { gas, gasLimit, gasPrice, condition = null }) {
this._api = api;
this.gas = gas;
this.gasLimit = gasLimit;
this.price = gasPrice;
if (condition) {
if (condition.block) {
this.condition = { block: condition.block.toFixed(0) };
this.conditionType = CONDITIONS.BLOCK;
} else if (condition.time) {
this.condition = { time: condition.time };
this.conditionType = CONDITIONS.TIME;
}
}
if (api) {
this.loadDefaults();
}
@ -54,6 +74,39 @@ export default class GasPriceEditor {
}
}
@action setConditionType = (conditionType = CONDITIONS.NONE) => {
transaction(() => {
this.conditionBlockError = null;
this.conditionType = conditionType;
switch (conditionType) {
case CONDITIONS.BLOCK:
this.condition = Object.assign({}, this.condition, { block: this.blockNumber || 1 });
break;
case CONDITIONS.TIME:
this.condition = Object.assign({}, this.condition, { time: new Date() });
break;
case CONDITIONS.NONE:
default:
this.condition = {};
break;
}
});
}
@action setConditionBlockNumber = (block) => {
transaction(() => {
this.conditionBlockError = validatePositiveNumber(block).numberError;
this.condition = Object.assign({}, this.condition, { block });
});
}
@action setConditionDateTime = (time) => {
this.condition = Object.assign({}, this.condition, { time });
}
@action setEditing = (isEditing) => {
this.isEditing = isEditing;
}
@ -130,9 +183,10 @@ export default class GasPriceEditor {
bucket_bounds: [],
counts: []
})),
this._api.eth.gasPrice()
this._api.eth.gasPrice(),
this._api.eth.blockNumber()
])
.then(([histogram, _price]) => {
.then(([histogram, _price, blockNumber]) => {
transaction(() => {
const price = _price.toFixed(0);
@ -142,6 +196,7 @@ export default class GasPriceEditor {
this.setHistogram(histogram);
this.priceDefault = price;
this.blockNumber = blockNumber.toNumber();
});
})
.catch((error) => {
@ -150,13 +205,37 @@ export default class GasPriceEditor {
}
overrideTransaction = (transaction) => {
if (this.errorGas || this.errorPrice) {
if (this.errorGas || this.errorPrice || this.conditionBlockError) {
return transaction;
}
return Object.assign({}, transaction, {
const override = {
condition: this.condition,
gas: new BigNumber(this.gas || DEFAULT_GAS),
gasPrice: new BigNumber(this.price || DEFAULT_GASPRICE)
});
};
const result = Object.assign({}, transaction, override);
switch (this.conditionType) {
case CONDITIONS.BLOCK:
result.condition = { block: new BigNumber(this.condition.block || 0) };
break;
case CONDITIONS.TIME:
result.condition = { time: this.condition.time };
break;
case CONDITIONS.NONE:
default:
delete result.condition;
break;
}
return result;
}
}
export {
CONDITIONS
};

View File

@ -21,6 +21,7 @@ import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/consta
import { ERRORS } from '~/util/validation';
import GasPriceEditor from './gasPriceEditor';
import { CONDITIONS } from './store';
const { Store } = GasPriceEditor;
@ -31,18 +32,30 @@ const HISTOGRAM = {
counts: [3, 4]
};
const api = {
eth: {
gasPrice: sinon.stub().resolves(GASPRICE)
},
parity: {
gasPriceHistogram: sinon.stub().resolves(HISTOGRAM)
}
};
let api;
describe('ui/GasPriceEditor/store', () => {
// TODO: share with gasPriceEditor.spec.js
function createApi () {
api = {
eth: {
blockNumber: sinon.stub().resolves(new BigNumber(2)),
gasPrice: sinon.stub().resolves(GASPRICE)
},
parity: {
gasPriceHistogram: sinon.stub().resolves(HISTOGRAM)
}
};
return api;
}
describe('ui/GasPriceEditor/Store', () => {
let store = null;
beforeEach(() => {
createApi();
});
it('is available via GasPriceEditor.Store', () => {
expect(new Store(null, {})).to.be.ok;
});
@ -65,6 +78,7 @@ describe('ui/GasPriceEditor/store', () => {
describe('constructor (defaults) when histogram not available', () => {
const api = {
eth: {
blockNumber: sinon.stub().resolves(new BigNumber(2)),
gasPrice: sinon.stub().resolves(GASPRICE)
},
parity: {
@ -92,6 +106,67 @@ describe('ui/GasPriceEditor/store', () => {
store = new Store(null, { gasLimit: GASLIMIT });
});
describe('setConditionType', () => {
it('sets the actual type', () => {
store.setConditionType('testingType');
expect(store.conditionType).to.equal('testingType');
});
it('clears any block error on changing type', () => {
store.setConditionBlockNumber(-1);
expect(store.conditionBlockError).not.to.be.null;
store.setConditionType(CONDITIONS.BLOCK);
expect(store.conditionBlockError).to.be.null;
});
it('sets condition.block when type === CONDITIONS.BLOCK', () => {
store.setConditionType(CONDITIONS.BLOCK);
expect(store.condition.block).to.be.ok;
});
it('clears condition when type === CONDITIONS.NONE', () => {
store.setConditionType(CONDITIONS.BLOCK);
store.setConditionType(CONDITIONS.NONE);
expect(store.condition).to.deep.equal({});
});
it('sets condition.time when type === CONDITIONS.TIME', () => {
store.setConditionType(CONDITIONS.TIME);
expect(store.condition.time).to.be.ok;
});
});
describe('setConditionBlockNumber', () => {
beforeEach(() => {
store.setConditionBlockNumber('testingBlock');
});
it('sets the blockNumber', () => {
expect(store.condition.block).to.equal('testingBlock');
});
it('sets the error on invalid numbers', () => {
expect(store.conditionBlockError).not.to.be.null;
});
it('sets the error on negative numbers', () => {
store.setConditionBlockNumber(-1);
expect(store.conditionBlockError).not.to.be.null;
});
it('clears the error on positive numbers', () => {
store.setConditionBlockNumber(1000);
expect(store.conditionBlockError).to.be.null;
});
});
describe('setConditionDateTime', () => {
it('sets the datatime', () => {
store.setConditionDateTime('testingDateTime');
expect(store.condition.time).to.equal('testingDateTime');
});
});
describe('setEditing', () => {
it('sets the value', () => {
expect(store.isEditing).to.be.false;

View File

@ -14,9 +14,10 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { CircularProgress } from 'material-ui';
import moment from 'moment';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import CircularProgress from 'material-ui/CircularProgress';
import { TypedInput, InputAddress } from '../Form';
import MethodDecodingStore from './methodDecodingStore';
@ -128,15 +129,25 @@ class MethodDecoding extends Component {
renderMinBlock () {
const { historic, transaction } = this.props;
const { minBlock } = transaction;
const { condition } = transaction;
if (!minBlock || minBlock.eq(0)) {
if (!condition) {
return null;
}
return (
<span>, { historic ? 'Submitted' : 'Submission' } at block <span className={ styles.highlight }>#{ minBlock.toFormat(0) }</span></span>
);
if (condition.block && condition.block.gt(0)) {
return (
<span>, { historic ? 'Submitted' : 'Submission' } at block <span className={ styles.highlight }>#{ condition.block.toFormat(0) }</span></span>
);
}
if (condition.time) {
return (
<span>, { historic ? 'Submitted' : 'Submission' } at <span className={ styles.highlight }>{ moment(condition.time).format('LLLL') }</span></span>
);
}
return null;
}
renderAction () {

View File

@ -47,20 +47,6 @@
.title {
background: rgba(0, 0, 0, 0.25) !important;
padding: 1em;
margin-bottom: 0;
h3 {
margin: 0;
text-transform: uppercase;
}
.steps {
margin-bottom: -1em;
}
}
.waiting {
margin: 1em -1em -1em -1em;
}
.overlay {

View File

@ -22,7 +22,7 @@ import { connect } from 'react-redux';
import { nodeOrStringProptype } from '~/util/proptypes';
import Container from '../Container';
import Title from './Title';
import Title from '../Title';
const ACTIONS_STYLE = { borderStyle: 'none' };
const TITLE_STYLE = { borderStyle: 'none' };
@ -63,11 +63,12 @@ class Modal extends Component {
const contentStyle = muiTheme.parity.getBackgroundStyle(null, settings.backgroundSeed);
const header = (
<Title
activeStep={ current }
busy={ busy }
current={ current }
busySteps={ waiting }
className={ styles.title }
steps={ steps }
title={ title }
waiting={ waiting }
/>
);
const classes = `${styles.dialog} ${className}`;

View File

@ -16,13 +16,14 @@
*/
$modalMargin: 1.5em;
$modalPadding: 1.5em;
$modalBackZ: 2500;
/* This should be the default case, the Portal used as a stand-alone modal */
$modalBottom: 15vh;
$modalBottom: $modalMargin;
$modalLeft: $modalMargin;
$modalRight: $modalMargin;
$modalTop: 0;
$modalTop: $modalMargin;
$modalZ: 3500;
/* This is the case where popped-up over another modal, Portal or otherwise */
@ -55,7 +56,9 @@ $popoverZ: 3600;
background-color: rgba(0, 0, 0, 1);
box-sizing: border-box;
display: flex;
padding: 1.5em;
flex-direction: column;
justify-content: space-between;
padding: $modalPadding;
position: fixed;
* {
@ -77,22 +80,48 @@ $popoverZ: 3600;
width: calc(100vw - $popoverLeft - $popoverRight);
z-index: $popoverZ;
}
}
.closeIcon {
font-size: 4em;
position: absolute;
right: 1rem;
top: 0.5rem;
z-index: 100;
.buttonRow {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-end;
padding: $modalPadding 0 0 0;
&, * {
height: 48px !important;
width: 48px !important;
button:not([disabled]) {
color: white !important;
svg {
fill: white !important;
}
}
}
&:hover {
cursor: pointer;
opacity: 0.5;
.childContainer {
flex: 1;
overflow-x: hidden;
overflow-y: auto;
}
.closeIcon {
font-size: 4em;
position: absolute;
right: 1rem;
top: 0.5rem;
z-index: 100;
&, * {
height: 48px !important;
width: 48px !important;
}
&:hover {
cursor: pointer;
opacity: 0.5;
}
}
.titleRow {
margin-bottom: $modalPadding;
}
}

View File

@ -16,6 +16,7 @@
import React, { Component } from 'react';
import { Button } from '~/ui';
import PlaygroundExample from '~/playground/playgroundExample';
import Modal from '../Modal';
@ -77,6 +78,29 @@ export default class PortalExample extends Component {
</Portal>
</div>
</PlaygroundExample>
<PlaygroundExample name='Portal with Buttons'>
<div>
<button onClick={ this.handleOpen(4) }>Open</button>
<Portal
activeStep={ 0 }
buttons={ [
<Button
key='close'
label='close'
onClick={ this.handleClose }
/>
] }
isChildModal
open={ open[4] || false }
onClose={ this.handleClose }
steps={ [ 'step 1', 'step 2' ] }
title='Portal with button'
>
<p>This is the fourth portal</p>
</Portal>
</div>
</PlaygroundExample>
</div>
);
}

View File

@ -20,8 +20,10 @@ import ReactDOM from 'react-dom';
import ReactPortal from 'react-portal';
import keycode from 'keycode';
import { nodeOrStringProptype } from '~/util/proptypes';
import { CloseIcon } from '~/ui/Icons';
import ParityBackground from '~/ui/ParityBackground';
import Title from '~/ui/Title';
import styles from './portal.css';
@ -29,14 +31,35 @@ export default class Portal extends Component {
static propTypes = {
onClose: PropTypes.func.isRequired,
open: PropTypes.bool.isRequired,
activeStep: PropTypes.number,
busy: PropTypes.bool,
busySteps: PropTypes.array,
buttons: PropTypes.array,
children: PropTypes.node,
className: PropTypes.string,
hideClose: PropTypes.bool,
isChildModal: PropTypes.bool,
onKeyDown: PropTypes.func
onKeyDown: PropTypes.func,
steps: PropTypes.array,
title: nodeOrStringProptype()
};
componentDidMount () {
this.setBodyOverflow(this.props.open);
}
componentWillReceiveProps (nextProps) {
if (nextProps.open !== this.props.open) {
this.setBodyOverflow(nextProps.open);
}
}
componentWillUnmount () {
this.setBodyOverflow(false);
}
render () {
const { children, className, isChildModal, open } = this.props;
const { activeStep, busy, busySteps, children, className, isChildModal, open, steps, title } = this.props;
if (!open) {
return null;
@ -69,32 +92,72 @@ export default class Portal extends Component {
onKeyUp={ this.handleKeyUp }
/>
<ParityBackground className={ styles.parityBackground } />
<div
className={ styles.closeIcon }
onClick={ this.handleClose }
>
<CloseIcon />
{ this.renderClose() }
<Title
activeStep={ activeStep }
busy={ busy }
busySteps={ busySteps }
className={ styles.titleRow }
steps={ steps }
title={ title }
/>
<div className={ styles.childContainer }>
{ children }
</div>
{ children }
{ this.renderButtons() }
</div>
</div>
</ReactPortal>
);
}
renderButtons () {
const { buttons } = this.props;
if (!buttons) {
return null;
}
return (
<div className={ styles.buttonRow }>
{ buttons }
</div>
);
}
renderClose () {
const { hideClose } = this.props;
if (hideClose) {
return null;
}
return (
<CloseIcon
className={ styles.closeIcon }
onClick={ this.handleClose }
/>
);
}
stopEvent = (event) => {
event.preventDefault();
event.stopPropagation();
}
handleClose = () => {
this.props.onClose();
const { hideClose, onClose } = this.props;
if (!hideClose) {
onClose();
}
}
handleKeyDown = (event) => {
const { onKeyDown } = this.props;
event.persist();
return onKeyDown
? onKeyDown(event)
: false;
@ -111,10 +174,11 @@ export default class Portal extends Component {
}
handleDOMAction = (ref, method) => {
const refItem = typeof ref === 'string'
? this.refs[ref]
: ref;
const element = ReactDOM.findDOMNode(refItem);
const element = ReactDOM.findDOMNode(
typeof ref === 'string'
? this.refs[ref]
: ref
);
if (!element || typeof element[method] !== 'function') {
console.warn('could not find', ref, 'or method', method);
@ -123,4 +187,12 @@ export default class Portal extends Component {
return element[method]();
}
setBodyOverflow (open) {
if (!this.props.isChildModal) {
document.body.style.overflow = open
? 'hidden'
: null;
}
}
}

View File

@ -44,4 +44,21 @@ describe('ui/Portal', () => {
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('title rendering', () => {
const TITLE = 'some test title';
let title;
beforeEach(() => {
title = render({ title: TITLE }).find('Title');
});
it('renders the specified title', () => {
expect(title).to.have.length(1);
});
it('renders the passed title', () => {
expect(title.props().title).to.equal(TITLE);
});
});
});

26
js/src/ui/Title/title.css Normal file
View File

@ -0,0 +1,26 @@
/* Copyright 2015-2017 Parity Technologies (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/>.
*/
.title {
.steps {
margin: -0.5em 0 -1em 0;
}
.waiting {
margin: 1em -1em -1em -1em;
}
}

View File

@ -14,35 +14,49 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { LinearProgress } from 'material-ui';
import { Step, Stepper, StepLabel } from 'material-ui/Stepper';
import React, { Component, PropTypes } from 'react';
// TODO: It would make sense (going forward) to replace all uses of
// ContainerTitle with this component. In that case the styles for the
// h3 (title) can be pulled from there. (As it stands the duplication
// between the 2 has been removed, but as a short-term DRY only)
import { Title as ContainerTitle } from '~/ui/Container';
import { nodeOrStringProptype } from '~/util/proptypes';
import styles from '../modal.css';
import styles from './title.css';
export default class Title extends Component {
static propTypes = {
activeStep: PropTypes.number,
busy: PropTypes.bool,
current: PropTypes.number,
busySteps: PropTypes.array,
className: PropTypes.string,
steps: PropTypes.array,
waiting: PropTypes.array,
title: nodeOrStringProptype()
}
render () {
const { current, steps, title } = this.props;
const { activeStep, className, steps, title } = this.props;
if (!title && !steps) {
return null;
}
return (
<div className={ styles.title }>
<h3>
{
<div
className={
[styles.title, className].join(' ')
}
>
<ContainerTitle
title={
steps
? steps[current]
? steps[activeStep || 0]
: title
}
</h3>
/>
{ this.renderSteps() }
{ this.renderWaiting() }
</div>
@ -50,7 +64,7 @@ export default class Title extends Component {
}
renderSteps () {
const { current, steps } = this.props;
const { activeStep, steps } = this.props;
if (!steps) {
return;
@ -58,7 +72,7 @@ export default class Title extends Component {
return (
<div className={ styles.steps }>
<Stepper activeStep={ current }>
<Stepper activeStep={ activeStep }>
{ this.renderTimeline() }
</Stepper>
</div>
@ -80,8 +94,8 @@ export default class Title extends Component {
}
renderWaiting () {
const { current, busy, waiting } = this.props;
const isWaiting = busy || (waiting || []).includes(current);
const { activeStep, busy, busySteps } = this.props;
const isWaiting = busy || (busySteps || []).includes(activeStep);
if (!isWaiting) {
return null;

View File

@ -35,7 +35,7 @@ import DappIcon from './DappIcon';
import Editor from './Editor';
import Errors from './Errors';
import Features, { FEATURES, FeaturesStore } from './Features';
import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form';
import Form, { AddressSelect, DappUrlInput, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form';
import GasPriceEditor from './GasPriceEditor';
import GasPriceSelector from './GasPriceSelector';
import Icons from './Icons';
@ -55,6 +55,7 @@ import SectionList from './SectionList';
import ShortenedHash from './ShortenedHash';
import SignerIcon from './SignerIcon';
import Tags from './Tags';
import Title from './Title';
import Tooltips, { Tooltip } from './Tooltips';
import TxHash from './TxHash';
import TxList from './TxList';
@ -79,8 +80,9 @@ export {
ContextProvider,
CopyToClipboard,
CurrencySymbol,
DappIcon,
DappCard,
DappIcon,
DappUrlInput,
Editor,
Errors,
FEATURES,
@ -95,9 +97,12 @@ export {
InputAddress,
InputAddressSelect,
InputChip,
InputDate,
InputInline,
InputTime,
IdentityIcon,
IdentityName,
Label,
LanguageSelector,
Loading,
MethodDecoding,
@ -116,6 +121,7 @@ export {
SectionList,
SignerIcon,
Tags,
Title,
Tooltip,
Tooltips,
TxHash,

60
js/src/util/dapplink.js Normal file
View File

@ -0,0 +1,60 @@
// Copyright 2015-2017 Parity Technologies (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 base32 from 'base32.js';
const BASE_URL = '.web.web3.site';
const ENCODER_OPTS = { type: 'crockford' };
export function encodePath (token, url) {
const encoder = new base32.Encoder(ENCODER_OPTS);
const chars = `${token}+${url}`
.split('')
.map((char) => char.charCodeAt(0));
return encoder
.write(chars) // add the characters to encode
.finalize(); // create the encoded string
}
export function encodeUrl (token, url) {
const encoded = encodePath(token, url)
.match(/.{1,63}/g) // split into 63-character chunks, max length is 64 for URLs parts
.join('.'); // add '.' between URL parts
return `${encoded}${BASE_URL}`;
}
// TODO: This export is really more a helper along the way of verifying the actual
// encoding (being able to decode test values from the node layer), than meant to
// be used as-is. Should the need arrise to decode URLs as well (instead of just
// producing), it would make sense to further split the output into the token/URL
export function decode (encoded) {
const decoder = new base32.Decoder(ENCODER_OPTS);
const sanitized = encoded
.replace(BASE_URL, '') // remove the BASE URL
.split('.') // split the string on the '.' (63-char boundaries)
.join(''); // combine without the '.'
return decoder
.write(sanitized) // add the string to decode
.finalize() // create the decoded buffer
.toString(); // create string from buffer
}
export {
BASE_URL
};

View File

@ -0,0 +1,83 @@
// Copyright 2015-2017 Parity Technologies (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 { BASE_URL, decode, encodePath, encodeUrl } from './dapplink';
const TEST_TOKEN = 'token';
const TEST_URL = 'https://parity.io';
const TEST_URL_LONG = 'http://some.very.very.very.long.long.long.domain.example.com';
const TEST_PREFIX = 'EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY';
const TEST_PREFIX_LONG = [
'EHQPPSBE5DM78X3G78QJYWVFDNJJWXK5E9WJWXK5E9WJWXK5E9WJWV3FDSKJWV3', 'FDSKJWV3FDSKJWS3FDNGPJVHECNW62VBGDHJJWRVFDM'
].join('.');
const TEST_RESULT = `${TEST_PREFIX}${BASE_URL}`;
const TEST_ENCODED = `${TEST_TOKEN}+${TEST_URL}`;
describe('util/ethlink', () => {
describe('decode', () => {
it('decodes into encoded url', () => {
expect(decode(TEST_PREFIX)).to.equal(TEST_ENCODED);
});
it('decodes full into encoded url', () => {
expect(decode(TEST_RESULT)).to.equal(TEST_ENCODED);
});
});
describe('encodePath', () => {
it('encodes a url/token combination', () => {
expect(encodePath(TEST_TOKEN, TEST_URL)).to.equal(TEST_PREFIX);
});
it('changes when token changes', () => {
expect(encodePath('test-token-2', TEST_URL)).not.to.equal(TEST_PREFIX);
});
it('changes when url changes', () => {
expect(encodePath(TEST_TOKEN, 'http://other.example.com')).not.to.equal(TEST_PREFIX);
});
});
describe('encodeUrl', () => {
it('encodes a url/token combination', () => {
expect(encodeUrl(TEST_TOKEN, TEST_URL)).to.equal(TEST_RESULT);
});
it('changes when token changes', () => {
expect(encodeUrl('test-token-2', TEST_URL)).not.to.equal(TEST_RESULT);
});
it('changes when url changes', () => {
expect(encodeUrl(TEST_TOKEN, 'http://other.example.com')).not.to.equal(TEST_RESULT);
});
describe('splitting', () => {
let encoded;
beforeEach(() => {
encoded = encodeUrl(TEST_TOKEN, TEST_URL_LONG);
});
it('splits long values into boundary parts', () => {
expect(encoded).to.equal(`${TEST_PREFIX_LONG}${BASE_URL}`);
});
it('first part 63 characters', () => {
expect(encoded.split('.')[0].length).to.equal(63);
});
});
});
});

View File

@ -0,0 +1,52 @@
/* Copyright 2015-2017 Parity Technologies (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/>.
*/
.body {
background: #f80;
color: white;
opacity: 1;
max-width: 500px;
padding: 1em 4em 1em 2em;
position: fixed;
right: 1.5em;
top: 1.5em;
z-index: 1000;
.button {
background: rgba(0, 0, 0, 0.5);
color: white !important;
svg {
fill: white !important;
}
}
.buttonrow {
text-align: right;
}
p {
color: white;
}
.close {
cursor: pointer;
position: absolute;
right: 1em;
top: 1em;
}
}

View File

@ -0,0 +1,74 @@
// Copyright 2015-2017 Parity Technologies (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 { observer } from 'mobx-react';
import React, { Component } from 'react';
import { FormattedMessage } from 'react-intl';
import { Button } from '~/ui';
import { CloseIcon, CheckIcon } from '~/ui/Icons';
import Store from './store';
import styles from './extension.css';
@observer
export default class Extension extends Component {
store = new Store();
render () {
const { showWarning } = this.store;
if (!showWarning) {
return null;
}
return (
<div className={ styles.body }>
<CloseIcon
className={ styles.close }
onClick={ this.onClose }
/>
<p>
<FormattedMessage
id='extension.intro'
defaultMessage='Parity now has an extension available for Chrome that allows safe browsing of Ethereum-enabled distributed applications. It is highly recommended that you install this extension to further enhance your Parity experience.'
/>
</p>
<p className={ styles.buttonrow }>
<Button
className={ styles.button }
icon={ <CheckIcon /> }
label={
<FormattedMessage
id='extension.install'
defaultMessage='Install the extension now'
/>
}
onClick={ this.onInstallClick }
/>
</p>
</div>
);
}
onClose = () => {
this.store.snoozeWarning();
}
onInstallClick = () => {
this.store.installExtension();
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (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 './extension';

View File

@ -0,0 +1,89 @@
// Copyright 2015-2017 Parity Technologies (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/>.
/* global chrome */
import { action, computed, observable } from 'mobx';
import store from 'store';
import browser from 'useragent.js/lib/browser';
const A_DAY = 24 * 60 * 60 * 1000;
const NEXT_DISPLAY = '_parity::extensionWarning::nextDisplay';
// 'https://chrome.google.com/webstore/detail/parity-ethereum-integrati/himekenlppkgeaoeddcliojfddemadig';
const EXTENSION_PAGE = 'https://chrome.google.com/webstore/detail/himekenlppkgeaoeddcliojfddemadig';
export default class Store {
@observable isInstalling = false;
@observable nextDisplay = 0;
@observable shouldInstall = false;
constructor () {
this.nextDisplay = store.get(NEXT_DISPLAY) || 0;
this.testInstall();
}
@computed get showWarning () {
return !this.isInstalling && this.shouldInstall && (Date.now() > this.nextDisplay);
}
@action setInstalling = (isInstalling) => {
this.isInstalling = isInstalling;
}
@action snoozeWarning = (sleep = A_DAY) => {
this.nextDisplay = Date.now() + sleep;
store.set(NEXT_DISPLAY, this.nextDisplay);
}
@action testInstall = () => {
this.shouldInstall = this.readStatus();
}
readStatus = () => {
const hasExtension = Symbol.for('parity.extension') in window;
const ua = browser.analyze(navigator.userAgent || '');
if (hasExtension) {
return false;
}
return (ua || {}).name.toLowerCase() === 'chrome';
}
installExtension = () => {
this.setInstalling(true);
return new Promise((resolve, reject) => {
const link = document.createElement('link');
link.setAttribute('rel', 'chrome-webstore-item');
link.setAttribute('href', EXTENSION_PAGE);
document.querySelector('head').appendChild(link);
if (chrome && chrome.webstore && chrome.webstore.install) {
chrome.webstore.install(EXTENSION_PAGE, resolve, reject);
} else {
reject(new Error('Direct installation failed.'));
}
})
.catch((error) => {
console.warn('Unable to perform direct install', error);
window.open(EXTENSION_PAGE, '_blank');
});
}
}

View File

@ -26,6 +26,7 @@ import ParityBar from '../ParityBar';
import Snackbar from './Snackbar';
import Container from './Container';
import DappContainer from './DappContainer';
import Extension from './Extension';
import FrameError from './FrameError';
import Status from './Status';
import Store from './store';
@ -106,6 +107,7 @@ class Application extends Component {
? <Status upgradeStore={ this.upgradeStore } />
: null
}
<Extension />
<Snackbar />
</Container>
);

View File

@ -15,6 +15,9 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
$overlayZ: 10000;
$modalZ: 10001;
.account {
display: flex;
flex: 1;
@ -61,7 +64,7 @@
bottom: 0;
left: 0;
background: rgba(255, 255, 255, 0.5);
z-index: 10000;
z-index: $overlayZ;
user-select: none;
}
@ -70,7 +73,7 @@
position: fixed;
font-size: 16px;
font-family: 'Roboto', sans-serif;
z-index: 10001;
z-index: $modalZ;
user-select: none;
}
@ -109,7 +112,8 @@
border-radius: 4px 4px 0 0;
display: flex;
flex-direction: column;
max-height: 50vh;
min-height: 30vh;
max-height: 80vh;
max-width: calc(100vw - 1em);
.content {

View File

@ -73,14 +73,14 @@ export default class TransactionMainDetails extends Component {
: transaction
}
/>
{ this.renderEditGas() }
{ this.renderEditTx() }
</div>
{ children }
</div>
);
}
renderEditGas () {
renderEditTx () {
const { gasStore } = this.props;
if (!gasStore) {
@ -91,7 +91,7 @@ export default class TransactionMainDetails extends Component {
<div className={ styles.editButtonRow }>
<Button
icon={ <MapsLocalGasStation /> }
label='Edit gas/gasPrice'
label='Edit conditions/gas/gasPrice'
onClick={ this.toggleGasEditor }
/>
</div>

View File

@ -45,6 +45,7 @@ export default class TransactionPending extends Component {
onReject: PropTypes.func.isRequired,
store: PropTypes.object.isRequired,
transaction: PropTypes.shape({
condition: PropTypes.object,
data: PropTypes.string,
from: PropTypes.string.isRequired,
gas: PropTypes.object.isRequired,
@ -59,6 +60,7 @@ export default class TransactionPending extends Component {
};
gasStore = new GasPriceEditor.Store(this.context.api, {
condition: this.props.transaction.condition,
gas: this.props.transaction.gas.toFixed(),
gasLimit: this.props.gasLimit,
gasPrice: this.props.transaction.gasPrice.toFixed()
@ -80,7 +82,7 @@ export default class TransactionPending extends Component {
render () {
return this.gasStore.isEditing
? this.renderGasEditor()
? this.renderTxEditor()
: this.renderTransaction();
}
@ -115,7 +117,7 @@ export default class TransactionPending extends Component {
);
}
renderGasEditor () {
renderTxEditor () {
const { className } = this.props;
return (
@ -133,15 +135,21 @@ export default class TransactionPending extends Component {
onConfirm = (data) => {
const { id, transaction } = this.props;
const { password, wallet } = data;
const { gas, gasPrice } = this.gasStore.overrideTransaction(transaction);
const { condition, gas, gasPrice } = this.gasStore.overrideTransaction(transaction);
this.props.onConfirm({
const options = {
gas,
gasPrice,
id,
password,
wallet
});
};
if (condition && (condition.block || condition.time)) {
options.condition = condition;
}
this.props.onConfirm(options);
}
onReject = () => {

View File

@ -14,101 +14,62 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import Refresh from 'material-ui/svg-icons/navigation/refresh';
import Close from 'material-ui/svg-icons/navigation/close';
import Subdirectory from 'material-ui/svg-icons/navigation/subdirectory-arrow-left';
import { Button } from '~/ui';
const KEY_ESC = 27;
const KEY_ENTER = 13;
import { Button, DappUrlInput } from '~/ui';
import { CloseIcon, RefreshIcon } from '~/ui/Icons';
@observer
export default class AddressBar extends Component {
static propTypes = {
className: PropTypes.string,
isLoading: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
onRefresh: PropTypes.func.isRequired,
url: PropTypes.string.isRequired
store: PropTypes.object.isRequired
};
state = {
currentUrl: this.props.url
};
componentWillReceiveProps (nextProps) {
if (this.props.url === nextProps.url) {
return;
}
this.setState({
currentUrl: nextProps.url
});
}
isPristine () {
return this.state.currentUrl === this.props.url;
}
render () {
const { isLoading } = this.props;
const { currentUrl } = this.state;
const isPristine = this.isPristine();
const { isLoading, isPristine, nextUrl } = this.props.store;
return (
<div className={ this.props.className }>
<Button
disabled={ isLoading }
onClick={ this.onRefreshUrl }
icon={
isLoading
? <Close />
: <Refresh />
? <CloseIcon />
: <RefreshIcon />
}
onClick={ this.onGo }
/>
<input
onChange={ this.onUpdateUrl }
onKeyDown={ this.onKey }
type='text'
value={ currentUrl }
<DappUrlInput
onChange={ this.onChangeUrl }
onGoto={ this.onGotoUrl }
onRestore={ this.onRestoreUrl }
url={ nextUrl }
/>
<Button
disabled={ isPristine }
onClick={ this.onGotoUrl }
icon={ <Subdirectory /> }
onClick={ this.onGo }
/>
</div>
);
}
onUpdateUrl = (ev) => {
this.setState({
currentUrl: ev.target.value
});
};
onRefreshUrl = () => {
this.props.store.reload();
}
onKey = (ev) => {
const key = ev.which;
onChangeUrl = (url) => {
this.props.store.setNextUrl(url);
}
if (key === KEY_ESC) {
this.setState({
currentUrl: this.props.url
});
return;
}
onGotoUrl = () => {
this.props.store.gotoUrl();
}
if (key === KEY_ENTER) {
this.onGo();
return;
}
};
onGo = () => {
if (this.isPristine()) {
this.props.onRefresh();
} else {
this.props.onChange(this.state.currentUrl);
}
};
onRestoreUrl = () => {
this.props.store.restoreUrl();
}
}

View File

@ -0,0 +1,48 @@
// Copyright 2015-2017 Parity Technologies (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 { shallow } from 'enzyme';
import React from 'react';
import AddressBar from './';
let component;
let store;
function createStore () {
store = {
nextUrl: 'https://parity.io'
};
return store;
}
function render (props = {}) {
component = shallow(
<AddressBar
className='testClass'
store={ createStore() }
/>
);
return component;
}
describe('views/Web/AddressBar', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
});

158
js/src/views/Web/store.js Normal file
View File

@ -0,0 +1,158 @@
// Copyright 2015-2017 Parity Technologies (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 { action, computed, observable, transaction } from 'mobx';
import localStore from 'store';
import { parse as parseUrl } from 'url';
import { encodePath, encodeUrl } from '~/util/dapplink';
const DEFAULT_URL = 'https://mkr.market';
const LS_LAST_ADDRESS = '_parity::webLastAddress';
const hasProtocol = /^https?:\/\//;
let instance = null;
export default class Store {
@observable counter = Date.now();
@observable currentUrl = null;
@observable history = [];
@observable isLoading = false;
@observable parsedUrl = null;
@observable nextUrl = null;
@observable token = null;
constructor (api) {
this._api = api;
this.nextUrl = this.currentUrl = this.loadLastUrl();
}
@computed get encodedPath () {
return `${this._api.dappsUrl}/web/${encodePath(this.token, this.currentUrl)}?t=${this.counter}`;
}
@computed get encodedUrl () {
return `http://${encodeUrl(this.token, this.currentUrl)}:${this._api.dappsPort}?t=${this.counter}`;
}
@computed get frameId () {
return `_web_iframe_${this.counter}`;
}
@computed get isPristine () {
return this.currentUrl === this.nextUrl;
}
@action gotoUrl = (_url) => {
transaction(() => {
let url = (_url || this.nextUrl).trim().replace(/\/+$/, '');
if (!hasProtocol.test(url)) {
url = `https://${url}`;
}
this.setNextUrl(url);
this.setCurrentUrl(this.nextUrl);
});
}
@action reload = () => {
transaction(() => {
this.setLoading(true);
this.counter = Date.now();
});
}
@action restoreUrl = () => {
this.setNextUrl(this.currentUrl);
}
@action setHistory = (history) => {
this.history = history;
}
@action setLoading = (isLoading) => {
this.isLoading = isLoading;
}
@action setToken = (token) => {
this.token = token;
}
@action setCurrentUrl = (_url) => {
const url = _url || this.currentUrl;
transaction(() => {
this.currentUrl = url;
this.parsedUrl = parseUrl(url);
this.saveLastUrl();
this.reload();
});
}
@action setNextUrl = (url) => {
this.nextUrl = url;
}
generateToken = () => {
this.setToken(null);
return this._api.signer
.generateWebProxyAccessToken()
.then((token) => {
this.setToken(token);
})
.catch((error) => {
console.warn('generateToken', error);
});
}
loadHistory = () => {
return this._api.parity
.listRecentDapps()
.then((apps) => {
this.setHistory(apps);
})
.catch((error) => {
console.warn('loadHistory', error);
});
}
loadLastUrl = () => {
return localStore.get(LS_LAST_ADDRESS) || DEFAULT_URL;
}
saveLastUrl = () => {
return localStore.set(LS_LAST_ADDRESS, this.currentUrl);
}
static get (api) {
if (!instance) {
instance = new Store(api);
}
return instance;
}
}
export {
DEFAULT_URL,
LS_LAST_ADDRESS
};

View File

@ -0,0 +1,202 @@
// Copyright 2015-2017 Parity Technologies (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 sinon from 'sinon';
import Store from './store';
const TEST_HISTORY = ['somethingA', 'somethingB'];
const TEST_TOKEN = 'testing-123';
const TEST_URL1 = 'http://some.test.domain.com';
const TEST_URL2 = 'http://something.different.com';
let api;
let store;
function createApi () {
api = {
dappsPort: 8080,
dappsUrl: 'http://home.web3.site:8080',
parity: {
listRecentDapps: sinon.stub().resolves(TEST_HISTORY)
},
signer: {
generateWebProxyAccessToken: sinon.stub().resolves(TEST_TOKEN)
}
};
return api;
}
function create () {
store = new Store(createApi());
return store;
}
describe('views/Web/Store', () => {
beforeEach(() => {
create();
});
describe('@action', () => {
describe('gotoUrl', () => {
it('uses the nextUrl when none specified', () => {
store.setNextUrl('https://parity.io');
store.gotoUrl();
expect(store.currentUrl).to.equal('https://parity.io');
});
it('adds https when no protocol', () => {
store.gotoUrl('google.com');
expect(store.currentUrl).to.equal('https://google.com');
});
});
describe('restoreUrl', () => {
it('sets the nextUrl to the currentUrl', () => {
store.setCurrentUrl(TEST_URL1);
store.setNextUrl(TEST_URL2);
store.restoreUrl();
expect(store.nextUrl).to.equal(TEST_URL1);
});
});
describe('setCurrentUrl', () => {
beforeEach(() => {
store.setCurrentUrl(TEST_URL1);
});
it('sets the url', () => {
expect(store.currentUrl).to.equal(TEST_URL1);
});
});
describe('setHistory', () => {
it('sets the history', () => {
store.setHistory(TEST_HISTORY);
expect(store.history.peek()).to.deep.equal(TEST_HISTORY);
});
});
describe('setLoading', () => {
beforeEach(() => {
store.setLoading(true);
});
it('sets the loading state (true)', () => {
expect(store.isLoading).to.be.true;
});
it('sets the loading state (false)', () => {
store.setLoading(false);
expect(store.isLoading).to.be.false;
});
});
describe('setNextUrl', () => {
it('sets the url', () => {
store.setNextUrl(TEST_URL1);
expect(store.nextUrl).to.equal(TEST_URL1);
});
});
describe('setToken', () => {
it('sets the token', () => {
store.setToken(TEST_TOKEN);
expect(store.token).to.equal(TEST_TOKEN);
});
});
});
describe('@computed', () => {
describe('encodedUrl', () => {
describe('encodedPath', () => {
it('encodes current', () => {
store.setCurrentUrl(TEST_URL1);
expect(store.encodedPath).to.match(
/http:\/\/home\.web3\.site:8080\/web\/DSTPRV1BD1T78W1T5WQQ6VVDCMQ78SBKEGQ68VVDC5MPWBK3DXPG\?t=[0-9]*$/
);
});
});
it('encodes current', () => {
store.setCurrentUrl(TEST_URL1);
expect(store.encodedUrl).to.match(
/^http:\/\/DSTPRV1BD1T78W1T5WQQ6VVDCMQ78SBKEGQ68VVDC5MPWBK3DXPG\.web\.web3\.site:8080\?t=[0-9]*$/
);
});
});
describe('frameId', () => {
it('creates an id', () => {
expect(store.frameId).to.be.ok;
});
});
describe('isPristine', () => {
it('is true when current === next', () => {
store.setCurrentUrl(TEST_URL1);
store.setNextUrl(TEST_URL1);
expect(store.isPristine).to.be.true;
});
it('is false when current !== next', () => {
store.setCurrentUrl(TEST_URL1);
store.setNextUrl(TEST_URL2);
expect(store.isPristine).to.be.false;
});
});
});
describe('operations', () => {
describe('generateToken', () => {
beforeEach(() => {
return store.generateToken();
});
it('calls signer_generateWebProxyAccessToken', () => {
expect(api.signer.generateWebProxyAccessToken).to.have.been.calledOnce;
});
it('sets the token as retrieved', () => {
expect(store.token).to.equal(TEST_TOKEN);
});
});
describe('loadHistory', () => {
beforeEach(() => {
return store.loadHistory();
});
it('calls parity_listRecentDapps', () => {
expect(api.parity.listRecentDapps).to.have.been.calledOnce;
});
it('sets the history as retrieved', () => {
expect(store.history.peek()).to.deep.equal(TEST_HISTORY);
});
});
});
});

View File

@ -14,19 +14,16 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import store from 'store';
import { parse as parseUrl, format as formatUrl } from 'url';
import { parse as parseQuery } from 'querystring';
import { FormattedMessage } from 'react-intl';
import AddressBar from './AddressBar';
import Store from './store';
import styles from './web.css';
const LS_LAST_ADDRESS = '_parity::webLastAddress';
const hasProtocol = /^https?:\/\//;
@observer
export default class Web extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
@ -36,120 +33,62 @@ export default class Web extends Component {
params: PropTypes.object.isRequired
}
state = {
displayedUrl: null,
isLoading: true,
token: null,
url: null
};
store = Store.get(this.context.api);
componentDidMount () {
const { api } = this.context;
const { params } = this.props;
api
.signer
.generateWebProxyAccessToken()
.then((token) => {
this.setState({ token });
});
this.setUrl(params.url);
this.store.gotoUrl(this.props.params.url);
return this.store.generateToken();
}
componentWillReceiveProps (props) {
this.setUrl(props.params.url);
this.store.gotoUrl(props.params.url);
}
setUrl = (url) => {
url = url || store.get(LS_LAST_ADDRESS) || 'https://mkr.market';
if (!hasProtocol.test(url)) {
url = `https://${url}`;
}
this.setState({ url, displayedUrl: url });
};
render () {
const { displayedUrl, isLoading, token } = this.state;
const { currentUrl, token } = this.store;
if (!token) {
return (
<div className={ styles.wrapper }>
<h1 className={ styles.loading }>
Requesting access token...
<FormattedMessage
id='web.requestToken'
defaultMessage='Requesting access token...'
/>
</h1>
</div>
);
}
const { dappsUrl } = this.context.api;
const { url } = this.state;
return currentUrl
? this.renderFrame()
: null;
}
if (!url || !token) {
return null;
}
const parsed = parseUrl(url);
const { protocol, host, path } = parsed;
const address = `${dappsUrl}/web/${token}/${protocol.slice(0, -1)}/${host}${path}`;
renderFrame () {
const { encodedPath, frameId } = this.store;
return (
<div className={ styles.wrapper }>
<AddressBar
className={ styles.url }
isLoading={ isLoading }
onChange={ this.onUrlChange }
onRefresh={ this.onRefresh }
url={ displayedUrl }
store={ this.store }
/>
<iframe
className={ styles.frame }
frameBorder={ 0 }
name={ name }
id={ frameId }
name={ frameId }
onLoad={ this.iframeOnLoad }
sandbox='allow-forms allow-same-origin allow-scripts'
scrolling='auto'
src={ address }
src={ encodedPath }
/>
</div>
);
}
onUrlChange = (url) => {
if (!hasProtocol.test(url)) {
url = `https://${url}`;
}
store.set(LS_LAST_ADDRESS, url);
this.setState({
isLoading: true,
displayedUrl: url,
url: url
});
};
onRefresh = () => {
const { displayedUrl } = this.state;
// Insert timestamp
// This is a hack to prevent caching.
const parsed = parseUrl(displayedUrl);
parsed.query = parseQuery(parsed.query);
parsed.query.t = Date.now().toString();
delete parsed.search;
this.setState({
isLoading: true,
url: formatUrl(parsed)
});
};
iframeOnLoad = () => {
this.setState({
isLoading: false
});
this.store.setLoading(false);
};
}

View File

@ -0,0 +1,56 @@
// Copyright 2015-2017 Parity Technologies (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 { shallow } from 'enzyme';
import React from 'react';
import Web from './';
const TEST_URL = 'https://mkr.market';
let api;
let component;
function createApi () {
api = {};
return api;
}
function render (url = TEST_URL) {
component = shallow(
<Web params={ { url } } />,
{
context: { api: createApi() }
}
);
return component;
}
describe('views/Web', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('renders loading with no token', () => {
expect(component.find('FormattedMessage').props().id).to.equal('web.requestToken');
});
});

View File

@ -242,7 +242,7 @@ fn execute_import(cmd: ImportBlockchain) -> Result<(), String> {
}
};
let informant = Arc::new(Informant::new(client.clone(), None, None, None, cmd.with_color));
let informant = Arc::new(Informant::new(client.clone(), None, None, None, None, cmd.with_color));
service.register_io_handler(informant).map_err(|_| "Unable to register informant handler".to_owned())?;
let do_import = |bytes| {

View File

@ -19,6 +19,7 @@ use std::sync::Arc;
use dir::default_data_path;
use ethcore::client::Client;
use ethcore_rpc::informant::RpcStats;
use ethsync::SyncProvider;
use hash_fetch::fetch::Client as FetchClient;
use helpers::replace_home;
@ -64,6 +65,7 @@ pub struct Dependencies {
pub remote: Remote,
pub fetch: FetchClient,
pub signer: Arc<SignerService>,
pub stats: Arc<RpcStats>,
}
pub fn new(configuration: Configuration, deps: Dependencies) -> Result<Option<WebappServer>, String> {
@ -174,7 +176,7 @@ mod server {
} else {
rpc_apis::ApiSet::UnsafeContext
};
let apis = rpc_apis::setup_rpc(Default::default(), deps.apis.clone(), api_set);
let apis = rpc_apis::setup_rpc(deps.stats, deps.apis.clone(), api_set);
let handler = RpcHandler::new(Arc::new(apis), deps.remote);
let start_result = match auth {
None => {

View File

@ -30,7 +30,8 @@ use ethcore::service::ClientIoMessage;
use ethcore::snapshot::service::Service as SnapshotService;
use ethcore::snapshot::{RestorationStatus, SnapshotService as SS};
use number_prefix::{binary_prefix, Standalone, Prefixed};
use ethcore_rpc::is_major_importing;
use ethcore_rpc::{is_major_importing};
use ethcore_rpc::informant::RpcStats;
use rlp::View;
pub struct Informant {
@ -41,6 +42,7 @@ pub struct Informant {
snapshot: Option<Arc<SnapshotService>>,
sync: Option<Arc<SyncProvider>>,
net: Option<Arc<ManageNetwork>>,
rpc_stats: Option<Arc<RpcStats>>,
last_import: Mutex<Instant>,
skipped: AtomicUsize,
skipped_txs: AtomicUsize,
@ -63,13 +65,20 @@ pub trait MillisecondDuration {
impl MillisecondDuration for Duration {
fn as_milliseconds(&self) -> u64 {
self.as_secs() * 1000 + self.subsec_nanos() as u64 / 1000000
self.as_secs() * 1000 + self.subsec_nanos() as u64 / 1_000_000
}
}
impl Informant {
/// Make a new instance potentially `with_color` output.
pub fn new(client: Arc<Client>, sync: Option<Arc<SyncProvider>>, net: Option<Arc<ManageNetwork>>, snapshot: Option<Arc<SnapshotService>>, with_color: bool) -> Self {
pub fn new(
client: Arc<Client>,
sync: Option<Arc<SyncProvider>>,
net: Option<Arc<ManageNetwork>>,
snapshot: Option<Arc<SnapshotService>>,
rpc_stats: Option<Arc<RpcStats>>,
with_color: bool,
) -> Self {
Informant {
report: RwLock::new(None),
last_tick: RwLock::new(Instant::now()),
@ -78,6 +87,7 @@ impl Informant {
snapshot: snapshot,
sync: sync,
net: net,
rpc_stats: rpc_stats,
last_import: Mutex::new(Instant::now()),
skipped: AtomicUsize::new(0),
skipped_txs: AtomicUsize::new(0),
@ -102,6 +112,7 @@ impl Informant {
let cache_info = self.client.blockchain_cache_info();
let network_config = self.net.as_ref().map(|n| n.network_config());
let sync_status = self.sync.as_ref().map(|s| s.status());
let rpc_stats = self.rpc_stats.as_ref();
let importing = is_major_importing(sync_status.map(|s| s.state), self.client.queue_info());
let (snapshot_sync, snapshot_current, snapshot_total) = self.snapshot.as_ref().map_or((false, 0, 0), |s|
@ -126,10 +137,10 @@ impl Informant {
false => t,
};
info!(target: "import", "{} {} {}",
info!(target: "import", "{} {} {} {}",
match importing {
true => match snapshot_sync {
false => format!("Syncing {} {} {} {}+{} Qed",
false => format!("Syncing {} {} {} {}+{} Qed",
paint(White.bold(), format!("{:>8}", format!("#{}", chain_info.best_block_number))),
paint(White.bold(), format!("{}", chain_info.best_block_hash)),
{
@ -170,7 +181,16 @@ impl Informant {
Some(ref sync_info) => format!(" {} sync", paint(Blue.bold(), format!("{:>8}", format_bytes(sync_info.mem_used)))),
_ => String::new(),
}
)
),
match rpc_stats {
Some(ref rpc_stats) => format!(
"RPC: {} conn, {} req/s, {} µs",
paint(Blue.bold(), format!("{:2}", rpc_stats.sessions())),
paint(Blue.bold(), format!("{:2}", rpc_stats.requests_rate())),
paint(Blue.bold(), format!("{:3}", rpc_stats.approximated_roundtrip())),
),
_ => String::new(),
},
);
*write_report = Some(report);

View File

@ -22,6 +22,7 @@ use io::PanicHandler;
use dir::default_data_path;
use ethcore_rpc::{self as rpc, RpcServerError, IpcServerError, Metadata};
use ethcore_rpc::informant::{RpcStats, Middleware};
use helpers::parity_ipc_path;
use jsonrpc_core::MetaIoHandler;
use jsonrpc_core::reactor::{RpcHandler, Remote};
@ -85,6 +86,7 @@ pub struct Dependencies {
pub panic_handler: Arc<PanicHandler>,
pub apis: Arc<rpc_apis::Dependencies>,
pub remote: Remote,
pub stats: Arc<RpcStats>,
}
pub fn new_http(conf: HttpConfiguration, deps: &Dependencies) -> Result<Option<HttpServer>, String> {
@ -97,8 +99,8 @@ pub fn new_http(conf: HttpConfiguration, deps: &Dependencies) -> Result<Option<H
Ok(Some(setup_http_rpc_server(deps, &addr, conf.cors, conf.hosts, conf.apis)?))
}
fn setup_apis(apis: ApiSet, deps: &Dependencies) -> MetaIoHandler<Metadata> {
rpc_apis::setup_rpc(MetaIoHandler::default(), deps.apis.clone(), apis)
fn setup_apis(apis: ApiSet, deps: &Dependencies) -> MetaIoHandler<Metadata, Middleware> {
rpc_apis::setup_rpc(deps.stats.clone(), deps.apis.clone(), apis)
}
pub fn setup_http_rpc_server(
@ -122,12 +124,12 @@ pub fn setup_http_rpc_server(
}
}
pub fn new_ipc(conf: IpcConfiguration, deps: &Dependencies) -> Result<Option<IpcServer<Metadata>>, String> {
pub fn new_ipc(conf: IpcConfiguration, deps: &Dependencies) -> Result<Option<IpcServer<Metadata, Middleware>>, String> {
if !conf.enabled { return Ok(None); }
Ok(Some(setup_ipc_rpc_server(deps, &conf.socket_addr, conf.apis)?))
}
pub fn setup_ipc_rpc_server(dependencies: &Dependencies, addr: &str, apis: ApiSet) -> Result<IpcServer<Metadata>, String> {
pub fn setup_ipc_rpc_server(dependencies: &Dependencies, addr: &str, apis: ApiSet) -> Result<IpcServer<Metadata, Middleware>, String> {
let apis = setup_apis(apis, dependencies);
let handler = RpcHandler::new(Arc::new(apis), dependencies.remote.clone());
match rpc::start_ipc(addr, handler) {

View File

@ -14,22 +14,25 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::cmp::PartialEq;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::cmp::PartialEq;
use std::str::FromStr;
use std::sync::Arc;
use util::RotatingLogger;
use jsonrpc_core::{MetaIoHandler};
use ethcore::miner::{Miner, ExternalMiner};
use ethcore::client::Client;
use ethcore::account_provider::AccountProvider;
use ethcore::snapshot::SnapshotService;
use ethsync::{ManageNetwork, SyncProvider};
use ethcore_rpc::{Metadata, NetworkSettings};
pub use ethcore_rpc::SignerService;
use updater::Updater;
use ethcore::account_provider::AccountProvider;
use ethcore::client::Client;
use ethcore::miner::{Miner, ExternalMiner};
use ethcore::snapshot::SnapshotService;
use ethcore_rpc::{Metadata, NetworkSettings};
use ethcore_rpc::informant::{Middleware, RpcStats, ClientNotifier};
use ethsync::{ManageNetwork, SyncProvider};
use hash_fetch::fetch::Client as FetchClient;
use jsonrpc_core::{MetaIoHandler};
use updater::Updater;
use util::RotatingLogger;
#[derive(Debug, PartialEq, Clone, Eq, Hash)]
pub enum Api {
@ -182,9 +185,13 @@ macro_rules! add_signing_methods {
}
}
pub fn setup_rpc(mut handler: MetaIoHandler<Metadata>, deps: Arc<Dependencies>, apis: ApiSet) -> MetaIoHandler<Metadata> {
pub fn setup_rpc(stats: Arc<RpcStats>, deps: Arc<Dependencies>, apis: ApiSet) -> MetaIoHandler<Metadata, Middleware> {
use ethcore_rpc::v1::*;
let mut handler = MetaIoHandler::with_middleware(Middleware::new(stats, ClientNotifier {
client: deps.client.clone(),
}));
// it's turned into vector, cause ont of the cases requires &[]
let apis = apis.list_apis().into_iter().collect::<Vec<_>>();
for api in &apis {
@ -244,7 +251,7 @@ pub fn setup_rpc(mut handler: MetaIoHandler<Metadata>, deps: Arc<Dependencies>,
add_signing_methods!(ParitySigning, handler, deps);
},
Api::ParityAccounts => {
handler.extend_with(ParityAccountsClient::new(&deps.secret_store, &deps.client).to_delegate());
handler.extend_with(ParityAccountsClient::new(&deps.secret_store).to_delegate());
},
Api::ParitySet => {
handler.extend_with(ParitySetClient::new(

View File

@ -18,7 +18,7 @@ use std::sync::Arc;
use std::net::{TcpListener};
use ctrlc::CtrlC;
use fdlimit::raise_fd_limit;
use ethcore_rpc::{NetworkSettings, is_major_importing};
use ethcore_rpc::{NetworkSettings, informant, is_major_importing};
use ethsync::NetworkConfiguration;
use util::{Colour, version, RotatingLogger, Mutex, Condvar};
use io::{MayPanic, ForwardPanic, PanicHandler};
@ -358,6 +358,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R
service.add_notify(updater.clone());
// set up dependencies for rpc servers
let rpc_stats = Arc::new(informant::RpcStats::default());
let signer_path = cmd.signer_conf.signer_path.clone();
let deps_for_rpc_apis = Arc::new(rpc_apis::Dependencies {
signer_service: Arc::new(rpc_apis::SignerService::new(move || {
@ -390,6 +391,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R
panic_handler: panic_handler.clone(),
apis: deps_for_rpc_apis.clone(),
remote: event_loop.raw_remote(),
stats: rpc_stats.clone(),
};
// start rpc servers
@ -405,6 +407,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R
remote: event_loop.raw_remote(),
fetch: fetch.clone(),
signer: deps_for_rpc_apis.signer_service.clone(),
stats: rpc_stats.clone(),
};
let dapps_server = dapps::new(cmd.dapps_conf.clone(), dapps_deps)?;
@ -413,6 +416,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R
panic_handler: panic_handler.clone(),
apis: deps_for_rpc_apis.clone(),
remote: event_loop.raw_remote(),
rpc_stats: rpc_stats.clone(),
};
let signer_server = signer::start(cmd.signer_conf.clone(), signer_deps)?;
@ -422,7 +426,8 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R
Some(sync_provider.clone()),
Some(manage_network.clone()),
Some(snapshot_service.clone()),
cmd.logger_config.color
Some(rpc_stats.clone()),
cmd.logger_config.color,
));
service.add_notify(informant.clone());
service.register_io_handler(informant.clone()).map_err(|_| "Unable to register informant handler".to_owned())?;

View File

@ -15,18 +15,21 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::io;
use std::sync::Arc;
use std::path::PathBuf;
use ansi_term::Colour;
use io::{ForwardPanic, PanicHandler};
use util::path::restrict_permissions_owner;
use rpc_apis;
use ethcore_signer as signer;
use dir::default_data_path;
use helpers::replace_home;
use jsonrpc_core::reactor::{RpcHandler, Remote};
use std::sync::Arc;
pub use ethcore_signer::Server as SignerServer;
use ansi_term::Colour;
use dir::default_data_path;
use ethcore_rpc::informant::RpcStats;
use ethcore_signer as signer;
use helpers::replace_home;
use io::{ForwardPanic, PanicHandler};
use jsonrpc_core::reactor::{RpcHandler, Remote};
use rpc_apis;
use util::path::restrict_permissions_owner;
const CODES_FILENAME: &'static str = "authcodes";
#[derive(Debug, PartialEq, Clone)]
@ -55,6 +58,7 @@ pub struct Dependencies {
pub panic_handler: Arc<PanicHandler>,
pub apis: Arc<rpc_apis::Dependencies>,
pub remote: Remote,
pub rpc_stats: Arc<RpcStats>,
}
pub struct NewToken {
@ -126,7 +130,8 @@ fn do_start(conf: Configuration, deps: Dependencies) -> Result<SignerServer, Str
info!("If you do not intend this, exit now.");
}
let server = server.skip_origin_validation(conf.skip_origin_validation);
let apis = rpc_apis::setup_rpc(Default::default(), deps.apis, rpc_apis::ApiSet::SafeContext);
let server = server.stats(deps.rpc_stats.clone());
let apis = rpc_apis::setup_rpc(deps.rpc_stats, deps.apis, rpc_apis::ApiSet::SafeContext);
let handler = RpcHandler::new(Arc::new(apis), deps.remote);
server.start(addr, handler)
};

View File

@ -17,6 +17,7 @@ serde_json = "0.8"
rustc-serialize = "0.3"
time = "0.1"
transient-hashmap = "0.1"
order-stat = "0.1"
jsonrpc-core = { git = "https://github.com/ethcore/jsonrpc.git" }
jsonrpc-http-server = { git = "https://github.com/ethcore/jsonrpc.git" }
jsonrpc-ipc-server = { git = "https://github.com/ethcore/jsonrpc.git" }

View File

@ -41,6 +41,7 @@ extern crate time;
extern crate rlp;
extern crate fetch;
extern crate futures;
extern crate order_stat;
extern crate parity_updater as updater;
extern crate parity_reactor;
@ -64,16 +65,16 @@ use jsonrpc_core::reactor::RpcHandler;
pub use ipc::{Server as IpcServer, Error as IpcServerError};
pub use jsonrpc_http_server::{ServerBuilder, Server, RpcServerError};
pub mod v1;
pub use v1::{SigningQueue, SignerService, ConfirmationsQueue, NetworkSettings, Metadata, Origin};
pub use v1::{SigningQueue, SignerService, ConfirmationsQueue, NetworkSettings, Metadata, Origin, informant};
pub use v1::block_import::is_major_importing;
/// Start http server asynchronously and returns result with `Server` handle on success or an error.
pub fn start_http<M: jsonrpc_core::Metadata>(
pub fn start_http<M: jsonrpc_core::Metadata, S: jsonrpc_core::Middleware<M>>(
addr: &SocketAddr,
cors_domains: Option<Vec<String>>,
allowed_hosts: Option<Vec<String>>,
panic_handler: Arc<PanicHandler>,
handler: RpcHandler<M>,
handler: RpcHandler<M, S>,
) -> Result<Server, RpcServerError> {
let cors_domains = cors_domains.map(|domains| {
@ -96,7 +97,10 @@ pub fn start_http<M: jsonrpc_core::Metadata>(
}
/// Start ipc server asynchronously and returns result with `Server` handle on success or an error.
pub fn start_ipc<M: jsonrpc_core::Metadata>(addr: &str, handler: RpcHandler<M>) -> Result<ipc::Server<M>, ipc::Error> {
pub fn start_ipc<M: jsonrpc_core::Metadata, S: jsonrpc_core::Middleware<M>>(
addr: &str,
handler: RpcHandler<M, S>,
) -> Result<ipc::Server<M, S>, ipc::Error> {
let server = ipc::Server::with_rpc_handler(addr, handler)?;
server.run_async()?;
Ok(server)

View File

@ -17,7 +17,7 @@
use std::fmt::Debug;
use std::ops::Deref;
use rlp;
use util::{Address, H256, U256, Uint, Bytes};
use util::{Address, H520, H256, U256, Uint, Bytes};
use util::bytes::ToPretty;
use util::sha3::Hashable;
@ -112,6 +112,14 @@ pub fn execute<C, M>(client: &C, miner: &M, accounts: &AccountProvider, payload:
ConfirmationPayload::Signature(address, data) => {
signature(accounts, address, data.sha3(), pass)
.map(|result| result
.map(|rsv| {
let mut vrs = [0u8; 65];
let rsv = rsv.as_ref();
vrs[0] = rsv[64] + 27;
vrs[1..33].copy_from_slice(&rsv[0..32]);
vrs[33..65].copy_from_slice(&rsv[32..64]);
H520(vrs)
})
.map(RpcH520::from)
.map(ConfirmationResponse::Signature)
)

View File

@ -0,0 +1,301 @@
// Copyright 2015-2017 Parity Technologies (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/>.
//! RPC Requests Statistics
use std::fmt;
use std::sync::Arc;
use std::sync::atomic::{self, AtomicUsize};
use std::time;
use futures::Future;
use jsonrpc_core as rpc;
use order_stat;
use util::RwLock;
const RATE_SECONDS: usize = 10;
const STATS_SAMPLES: usize = 60;
struct RateCalculator {
era: time::Instant,
samples: [u16; RATE_SECONDS],
}
impl fmt::Debug for RateCalculator {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "{} req/s", self.rate())
}
}
impl Default for RateCalculator {
fn default() -> Self {
RateCalculator {
era: time::Instant::now(),
samples: [0; RATE_SECONDS],
}
}
}
impl RateCalculator {
fn elapsed(&self) -> u64 {
self.era.elapsed().as_secs()
}
pub fn tick(&mut self) -> u16 {
if self.elapsed() >= RATE_SECONDS as u64 {
self.era = time::Instant::now();
self.samples[0] = 0;
}
let pos = self.elapsed() as usize % RATE_SECONDS;
let next = (pos + 1) % RATE_SECONDS;
self.samples[next] = 0;
self.samples[pos] = self.samples[pos].saturating_add(1);
self.samples[pos]
}
fn current_rate(&self) -> usize {
let now = match self.elapsed() {
i if i >= RATE_SECONDS as u64 => RATE_SECONDS,
i => i as usize + 1,
};
let sum: usize = self.samples[0..now].iter().map(|x| *x as usize).sum();
sum / now
}
pub fn rate(&self) -> usize {
if self.elapsed() > RATE_SECONDS as u64 {
0
} else {
self.current_rate()
}
}
}
struct StatsCalculator<T = u32> {
filled: bool,
idx: usize,
samples: [T; STATS_SAMPLES],
}
impl<T: Default + Copy> Default for StatsCalculator<T> {
fn default() -> Self {
StatsCalculator {
filled: false,
idx: 0,
samples: [T::default(); STATS_SAMPLES],
}
}
}
impl<T: fmt::Display + Default + Copy + Ord> fmt::Debug for StatsCalculator<T> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "median: {} ms", self.approximated_median())
}
}
impl<T: Default + Copy + Ord> StatsCalculator<T> {
pub fn add(&mut self, sample: T) {
self.idx += 1;
if self.idx >= STATS_SAMPLES {
self.filled = true;
self.idx = 0;
}
self.samples[self.idx] = sample;
}
/// Returns aproximate of media
pub fn approximated_median(&self) -> T {
let mut copy = [T::default(); STATS_SAMPLES];
copy.copy_from_slice(&self.samples);
let bound = if self.filled { STATS_SAMPLES } else { self.idx + 1 };
let (_, &mut median) = order_stat::median_of_medians(&mut copy[0..bound]);
median
}
}
/// RPC Statistics
#[derive(Default, Debug)]
pub struct RpcStats {
requests: RwLock<RateCalculator>,
roundtrips: RwLock<StatsCalculator<u32>>,
active_sessions: AtomicUsize,
}
impl RpcStats {
/// Count session opened
pub fn open_session(&self) {
self.active_sessions.fetch_add(1, atomic::Ordering::SeqCst);
}
/// Count session closed.
/// Silently overflows if closing unopened session.
pub fn close_session(&self) {
self.active_sessions.fetch_sub(1, atomic::Ordering::SeqCst);
}
/// Count request. Returns number of requests in current second.
pub fn count_request(&self) -> u16 {
self.requests.write().tick()
}
/// Add roundtrip time (microseconds)
pub fn add_roundtrip(&self, microseconds: u32) {
self.roundtrips.write().add(microseconds)
}
/// Returns number of open sessions
pub fn sessions(&self) -> usize {
self.active_sessions.load(atomic::Ordering::Relaxed)
}
/// Returns requests rate
pub fn requests_rate(&self) -> usize {
self.requests.read().rate()
}
/// Returns approximated roundtrip in microseconds
pub fn approximated_roundtrip(&self) -> u32 {
self.roundtrips.read().approximated_median()
}
}
/// Notifies about RPC activity.
pub trait ActivityNotifier: Send + Sync + 'static {
/// Activity on RPC interface
fn active(&self);
}
/// Stats-counting RPC middleware
pub struct Middleware<T: ActivityNotifier = ClientNotifier> {
stats: Arc<RpcStats>,
notifier: T,
}
impl<T: ActivityNotifier> Middleware<T> {
/// Create new Middleware with stats counter and activity notifier.
pub fn new(stats: Arc<RpcStats>, notifier: T) -> Self {
Middleware {
stats: stats,
notifier: notifier,
}
}
fn as_micro(dur: time::Duration) -> u32 {
(dur.as_secs() * 1_000_000) as u32 + dur.subsec_nanos() / 1_000
}
}
impl<M: rpc::Metadata, T: ActivityNotifier> rpc::Middleware<M> for Middleware<T> {
fn on_request<F>(&self, request: rpc::Request, meta: M, process: F) -> rpc::FutureResponse where
F: FnOnce(rpc::Request, M) -> rpc::FutureResponse,
{
let start = time::Instant::now();
let response = process(request, meta);
self.notifier.active();
let stats = self.stats.clone();
stats.count_request();
response.map(move |res| {
stats.add_roundtrip(Self::as_micro(start.elapsed()));
res
}).boxed()
}
}
/// Client Notifier
pub struct ClientNotifier {
/// Client
pub client: Arc<::ethcore::client::Client>,
}
impl ActivityNotifier for ClientNotifier {
fn active(&self) {
self.client.keep_alive()
}
}
#[cfg(test)]
mod tests {
use super::{RateCalculator, StatsCalculator, RpcStats};
#[test]
fn should_calculate_rate() {
// given
let mut avg = RateCalculator::default();
// when
avg.tick();
avg.tick();
avg.tick();
let rate = avg.rate();
// then
assert_eq!(rate, 3usize);
}
#[test]
fn should_approximate_median() {
// given
let mut stats = StatsCalculator::default();
stats.add(5);
stats.add(100);
stats.add(3);
stats.add(15);
stats.add(20);
stats.add(6);
// when
let median = stats.approximated_median();
// then
assert_eq!(median, 5);
}
#[test]
fn should_count_rpc_stats() {
// given
let stats = RpcStats::default();
assert_eq!(stats.sessions(), 0);
assert_eq!(stats.requests_rate(), 0);
assert_eq!(stats.approximated_roundtrip(), 0);
// when
stats.open_session();
stats.close_session();
stats.open_session();
stats.count_request();
stats.count_request();
stats.add_roundtrip(125);
// then
assert_eq!(stats.sessions(), 1);
assert_eq!(stats.requests_rate(), 2);
assert_eq!(stats.approximated_roundtrip(), 125);
}
#[test]
fn should_be_sync_and_send() {
let stats = RpcStats::default();
is_sync(stats);
}
fn is_sync<F: Send + Sync>(x: F) {
drop(x)
}
}

View File

@ -17,16 +17,18 @@
#[macro_use]
pub mod errors;
pub mod dispatch;
pub mod block_import;
pub mod dispatch;
pub mod informant;
mod network_settings;
mod poll_manager;
mod poll_filter;
mod requests;
mod signer;
mod signing_queue;
mod network_settings;
pub use self::network_settings::NetworkSettings;
pub use self::poll_manager::PollManager;
pub use self::poll_filter::{PollFilter, limit_logs};
pub use self::requests::{
@ -36,4 +38,3 @@ pub use self::signing_queue::{
ConfirmationsQueue, ConfirmationPromise, ConfirmationResult, SigningQueue, QueueEvent, DefaultAccount,
};
pub use self::signer::SignerService;
pub use self::network_settings::NetworkSettings;

View File

@ -258,20 +258,6 @@ fn check_known<C>(client: &C, number: BlockNumber) -> Result<(), Error> where C:
const MAX_QUEUE_SIZE_TO_MINE_ON: usize = 4; // because uncles go back 6.
impl<C, SN: ?Sized, S: ?Sized, M, EM> EthClient<C, SN, S, M, EM> where
C: MiningBlockChainClient + 'static,
SN: SnapshotService + 'static,
S: SyncProvider + 'static,
M: MinerService + 'static,
EM: ExternalMinerService + 'static {
fn active(&self) -> Result<(), Error> {
// TODO: only call every 30s at most.
take_weak!(self.client).keep_alive();
Ok(())
}
}
#[cfg(windows)]
static SOLC: &'static str = "solc.exe";
@ -288,8 +274,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
type Metadata = Metadata;
fn protocol_version(&self) -> Result<String, Error> {
self.active()?;
let version = take_weak!(self.sync).status().protocol_version.to_owned();
Ok(format!("{}", version))
}
@ -297,7 +281,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
fn syncing(&self) -> Result<SyncStatus, Error> {
use ethcore::snapshot::RestorationStatus;
self.active()?;
let status = take_weak!(self.sync).status();
let client = take_weak!(self.client);
let snapshot_status = take_weak!(self.snapshot).status();
@ -331,8 +314,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
let dapp = meta.dapp_id.unwrap_or_default();
let author = move || {
self.active()?;
let mut miner = take_weak!(self.miner).author();
if miner == 0.into() {
let accounts = self.dapp_accounts(dapp.into())?;
@ -348,20 +329,14 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
}
fn is_mining(&self) -> Result<bool, Error> {
self.active()?;
Ok(take_weak!(self.miner).is_sealing())
}
fn hashrate(&self) -> Result<RpcU256, Error> {
self.active()?;
Ok(RpcU256::from(self.external_miner.hashrate()))
}
fn gas_price(&self) -> Result<RpcU256, Error> {
self.active()?;
let (client, miner) = (take_weak!(self.client), take_weak!(self.miner));
Ok(RpcU256::from(default_gas_price(&*client, &*miner)))
}
@ -370,8 +345,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
let dapp = meta.dapp_id.unwrap_or_default();
let accounts = move || {
self.active()?;
let accounts = self.dapp_accounts(dapp.into())?;
Ok(accounts.into_iter().map(Into::into).collect())
};
@ -380,8 +353,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
}
fn block_number(&self) -> Result<RpcU256, Error> {
self.active()?;
Ok(RpcU256::from(take_weak!(self.client).chain_info().best_block_number))
}
@ -389,7 +360,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
let address = address.into();
let inner = || {
self.active()?;
match num.0.clone() {
BlockNumber::Pending => Ok(take_weak!(self.miner).balance(&*take_weak!(self.client), &address).into()),
id => {
@ -412,7 +382,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
let position: U256 = RpcU256::into(pos);
let inner = || {
self.active()?;
match num.0.clone() {
BlockNumber::Pending => Ok(take_weak!(self.miner).storage_at(&*take_weak!(self.client), &address, &H256::from(position)).into()),
id => {
@ -433,7 +402,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
fn transaction_count(&self, address: RpcH160, num: Trailing<BlockNumber>) -> BoxFuture<RpcU256, Error> {
let address: Address = RpcH160::into(address);
let inner = move || {
self.active()?;
match num.0.clone() {
BlockNumber::Pending => Ok(take_weak!(self.miner).nonce(&*take_weak!(self.client), &address).into()),
id => {
@ -453,7 +421,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
fn block_transaction_count_by_hash(&self, hash: RpcH256) -> BoxFuture<Option<RpcU256>, Error> {
let inner = || {
self.active()?;
Ok(take_weak!(self.client).block(BlockId::Hash(hash.into()))
.map(|block| block.transactions_count().into()))
};
@ -463,7 +430,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
fn block_transaction_count_by_number(&self, num: BlockNumber) -> BoxFuture<Option<RpcU256>, Error> {
let inner = || {
self.active()?;
match num {
BlockNumber::Pending => Ok(Some(
take_weak!(self.miner).status().transactions_in_pending_block.into()
@ -480,7 +446,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
fn block_uncles_count_by_hash(&self, hash: RpcH256) -> BoxFuture<Option<RpcU256>, Error> {
let inner = || {
self.active()?;
Ok(take_weak!(self.client).block(BlockId::Hash(hash.into()))
.map(|block| block.uncles_count().into()))
};
@ -490,7 +455,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
fn block_uncles_count_by_number(&self, num: BlockNumber) -> BoxFuture<Option<RpcU256>, Error> {
let inner = || {
self.active()?;
match num {
BlockNumber::Pending => Ok(Some(0.into())),
_ => Ok(
@ -507,7 +471,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
let address: Address = RpcH160::into(address);
let inner = || {
self.active()?;
match num.0.clone() {
BlockNumber::Pending => Ok(take_weak!(self.miner).code(&*take_weak!(self.client), &address).map_or_else(Bytes::default, Bytes::new)),
id => {
@ -526,25 +489,14 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
}
fn block_by_hash(&self, hash: RpcH256, include_txs: bool) -> BoxFuture<Option<RichBlock>, Error> {
let inner = || {
self.active()?;
self.block(BlockId::Hash(hash.into()), include_txs)
};
future::done(inner()).boxed()
future::done(self.block(BlockId::Hash(hash.into()), include_txs)).boxed()
}
fn block_by_number(&self, num: BlockNumber, include_txs: bool) -> BoxFuture<Option<RichBlock>, Error> {
let inner = || {
self.active()?;
self.block(num.into(), include_txs)
};
future::done(inner()).boxed()
future::done(self.block(num.into(), include_txs)).boxed()
}
fn transaction_by_hash(&self, hash: RpcH256) -> Result<Option<Transaction>, Error> {
self.active()?;
let hash: H256 = hash.into();
let miner = take_weak!(self.miner);
let client = take_weak!(self.client);
@ -552,20 +504,14 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
}
fn transaction_by_block_hash_and_index(&self, hash: RpcH256, index: Index) -> Result<Option<Transaction>, Error> {
self.active()?;
self.transaction(TransactionId::Location(BlockId::Hash(hash.into()), index.value()))
}
fn transaction_by_block_number_and_index(&self, num: BlockNumber, index: Index) -> Result<Option<Transaction>, Error> {
self.active()?;
self.transaction(TransactionId::Location(num.into(), index.value()))
}
fn transaction_receipt(&self, hash: RpcH256) -> Result<Option<Receipt>, Error> {
self.active()?;
let miner = take_weak!(self.miner);
let best_block = take_weak!(self.client).chain_info().best_block_number;
let hash: H256 = hash.into();
@ -580,20 +526,14 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
}
fn uncle_by_block_hash_and_index(&self, hash: RpcH256, index: Index) -> Result<Option<RichBlock>, Error> {
self.active()?;
self.uncle(UncleId { block: BlockId::Hash(hash.into()), position: index.value() })
}
fn uncle_by_block_number_and_index(&self, num: BlockNumber, index: Index) -> Result<Option<RichBlock>, Error> {
self.active()?;
self.uncle(UncleId { block: num.into(), position: index.value() })
}
fn compilers(&self) -> Result<Vec<String>, Error> {
self.active()?;
let mut compilers = vec![];
if Command::new(SOLC).output().is_ok() {
compilers.push("solidity".to_owned())
@ -622,7 +562,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
}
fn work(&self, no_new_work_timeout: Trailing<u64>) -> Result<Work, Error> {
self.active()?;
let no_new_work_timeout = no_new_work_timeout.0;
let client = take_weak!(self.client);
@ -674,8 +613,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
}
fn submit_work(&self, nonce: RpcH64, pow_hash: RpcH256, mix_hash: RpcH256) -> Result<bool, Error> {
self.active()?;
let nonce: H64 = nonce.into();
let pow_hash: H256 = pow_hash.into();
let mix_hash: H256 = mix_hash.into();
@ -688,14 +625,11 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
}
fn submit_hashrate(&self, rate: RpcU256, id: RpcH256) -> Result<bool, Error> {
self.active()?;
self.external_miner.submit_hashrate(rate.into(), id.into());
Ok(true)
}
fn send_raw_transaction(&self, raw: Bytes) -> Result<RpcH256, Error> {
self.active()?;
UntrustedRlp::new(&raw.into_vec()).as_val()
.map_err(errors::from_rlp_error)
.and_then(|tx| SignedTransaction::new(tx).map_err(errors::from_transaction_error))
@ -710,8 +644,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
}
fn call(&self, request: CallRequest, num: Trailing<BlockNumber>) -> Result<Bytes, Error> {
self.active()?;
let request = CallRequest::into(request);
let signed = self.sign_call(request)?;
@ -726,8 +658,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
}
fn estimate_gas(&self, request: CallRequest, num: Trailing<BlockNumber>) -> Result<RpcU256, Error> {
self.active()?;
let request = CallRequest::into(request);
let signed = self.sign_call(request)?;
take_weak!(self.client).estimate_gas(&signed, num.0.into())
@ -736,19 +666,14 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
}
fn compile_lll(&self, _: String) -> Result<Bytes, Error> {
self.active()?;
rpc_unimplemented!()
}
fn compile_serpent(&self, _: String) -> Result<Bytes, Error> {
self.active()?;
rpc_unimplemented!()
}
fn compile_solidity(&self, code: String) -> Result<Bytes, Error> {
self.active()?;
let maybe_child = Command::new(SOLC)
.arg("--bin")
.arg("--optimize")

View File

@ -50,19 +50,12 @@ impl<C, M> EthFilterClient<C, M> where
polls: Mutex::new(PollManager::new()),
}
}
fn active(&self) -> Result<(), Error> {
// TODO: only call every 30s at most.
take_weak!(self.client).keep_alive();
Ok(())
}
}
impl<C, M> EthFilter for EthFilterClient<C, M>
where C: BlockChainClient + 'static, M: MinerService + 'static
{
fn new_filter(&self, filter: Filter) -> Result<RpcU256, Error> {
self.active()?;
let mut polls = self.polls.lock();
let block_number = take_weak!(self.client).chain_info().best_block_number;
let id = polls.create_poll(PollFilter::Logs(block_number, Default::default(), filter));
@ -70,16 +63,12 @@ impl<C, M> EthFilter for EthFilterClient<C, M>
}
fn new_block_filter(&self) -> Result<RpcU256, Error> {
self.active()?;
let mut polls = self.polls.lock();
let id = polls.create_poll(PollFilter::Block(take_weak!(self.client).chain_info().best_block_number));
Ok(id.into())
}
fn new_pending_transaction_filter(&self) -> Result<RpcU256, Error> {
self.active()?;
let mut polls = self.polls.lock();
let best_block = take_weak!(self.client).chain_info().best_block_number;
let pending_transactions = take_weak!(self.miner).pending_transactions_hashes(best_block);
@ -88,7 +77,6 @@ impl<C, M> EthFilter for EthFilterClient<C, M>
}
fn filter_changes(&self, index: Index) -> Result<FilterChanges, Error> {
self.active()?;
let client = take_weak!(self.client);
let mut polls = self.polls.lock();
match polls.poll_mut(&index.value()) {
@ -180,8 +168,6 @@ impl<C, M> EthFilter for EthFilterClient<C, M>
}
fn filter_logs(&self, index: Index) -> Result<Vec<Log>, Error> {
self.active()?;
let mut polls = self.polls.lock();
match polls.poll(&index.value()) {
Some(&PollFilter::Logs(ref _block_number, ref _previous_log, ref filter)) => {
@ -207,8 +193,6 @@ impl<C, M> EthFilter for EthFilterClient<C, M>
}
fn uninstall_filter(&self, index: Index) -> Result<bool, Error> {
self.active()?;
self.polls.lock().remove_poll(&index.value());
Ok(true)
}

View File

@ -101,12 +101,6 @@ impl<C, M, S: ?Sized, U> ParityClient<C, M, S, U> where
dapps_port: dapps_port,
}
}
fn active(&self) -> Result<(), Error> {
// TODO: only call every 30s at most.
take_weak!(self.client).keep_alive();
Ok(())
}
}
impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
@ -118,8 +112,6 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
type Metadata = Metadata;
fn accounts_info(&self, dapp: Trailing<DappId>) -> Result<BTreeMap<String, BTreeMap<String, String>>, Error> {
self.active()?;
let dapp = dapp.0;
let store = take_weak!(self.accounts);
@ -149,8 +141,6 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
fn default_account(&self, meta: Self::Metadata) -> BoxFuture<H160, Error> {
let dapp_id = meta.dapp_id.unwrap_or_default();
let default_account = move || {
self.active()?;
Ok(take_weak!(self.accounts)
.dapps_addresses(dapp_id.into())
.ok()
@ -163,57 +153,39 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
}
fn transactions_limit(&self) -> Result<usize, Error> {
self.active()?;
Ok(take_weak!(self.miner).transactions_limit())
}
fn min_gas_price(&self) -> Result<U256, Error> {
self.active()?;
Ok(U256::from(take_weak!(self.miner).minimal_gas_price()))
}
fn extra_data(&self) -> Result<Bytes, Error> {
self.active()?;
Ok(Bytes::new(take_weak!(self.miner).extra_data()))
}
fn gas_floor_target(&self) -> Result<U256, Error> {
self.active()?;
Ok(U256::from(take_weak!(self.miner).gas_floor_target()))
}
fn gas_ceil_target(&self) -> Result<U256, Error> {
self.active()?;
Ok(U256::from(take_weak!(self.miner).gas_ceil_target()))
}
fn dev_logs(&self) -> Result<Vec<String>, Error> {
self.active()?;
let logs = self.logger.logs();
Ok(logs.as_slice().to_owned())
}
fn dev_logs_levels(&self) -> Result<String, Error> {
self.active()?;
Ok(self.logger.levels().to_owned())
}
fn net_chain(&self) -> Result<String, Error> {
self.active()?;
Ok(self.settings.chain.clone())
}
fn net_peers(&self) -> Result<Peers, Error> {
self.active()?;
let sync = take_weak!(self.sync);
let sync_status = sync.status();
let net_config = take_weak!(self.net).network_config();
@ -228,20 +200,14 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
}
fn net_port(&self) -> Result<u16, Error> {
self.active()?;
Ok(self.settings.network_port)
}
fn node_name(&self) -> Result<String, Error> {
self.active()?;
Ok(self.settings.name.clone())
}
fn registry_address(&self) -> Result<Option<H160>, Error> {
self.active()?;
Ok(
take_weak!(self.client)
.additional_params()
@ -252,7 +218,6 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
}
fn rpc_settings(&self) -> Result<RpcSettings, Error> {
self.active()?;
Ok(RpcSettings {
enabled: self.settings.rpc_enabled,
interface: self.settings.rpc_interface.clone(),
@ -261,19 +226,14 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
}
fn default_extra_data(&self) -> Result<Bytes, Error> {
self.active()?;
Ok(Bytes::new(version_data()))
}
fn gas_price_histogram(&self) -> Result<Histogram, Error> {
self.active()?;
take_weak!(self.client).gas_price_histogram(100, 10).ok_or_else(errors::not_enough_data).map(Into::into)
}
fn unsigned_transactions_count(&self) -> Result<usize, Error> {
self.active()?;
match self.signer {
None => Err(errors::signer_disabled()),
Some(ref signer) => Ok(signer.len()),
@ -281,56 +241,40 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
}
fn generate_secret_phrase(&self) -> Result<String, Error> {
self.active()?;
Ok(random_phrase(12))
}
fn phrase_to_address(&self, phrase: String) -> Result<H160, Error> {
self.active()?;
Ok(Brain::new(phrase).generate().unwrap().address().into())
}
fn list_accounts(&self, count: u64, after: Option<H160>, block_number: Trailing<BlockNumber>) -> Result<Option<Vec<H160>>, Error> {
self.active()?;
Ok(take_weak!(self.client)
.list_accounts(block_number.0.into(), after.map(Into::into).as_ref(), count)
.map(|a| a.into_iter().map(Into::into).collect()))
}
fn list_storage_keys(&self, address: H160, count: u64, after: Option<H256>, block_number: Trailing<BlockNumber>) -> Result<Option<Vec<H256>>, Error> {
self.active()?;
Ok(take_weak!(self.client)
.list_storage(block_number.0.into(), &address.into(), after.map(Into::into).as_ref(), count)
.map(|a| a.into_iter().map(Into::into).collect()))
}
fn encrypt_message(&self, key: H512, phrase: Bytes) -> Result<Bytes, Error> {
self.active()?;
ecies::encrypt(&key.into(), &DEFAULT_MAC, &phrase.0)
.map_err(errors::encryption_error)
.map(Into::into)
}
fn pending_transactions(&self) -> Result<Vec<Transaction>, Error> {
self.active()?;
Ok(take_weak!(self.miner).pending_transactions().into_iter().map(Into::into).collect::<Vec<_>>())
}
fn future_transactions(&self) -> Result<Vec<Transaction>, Error> {
self.active()?;
Ok(take_weak!(self.miner).future_transactions().into_iter().map(Into::into).collect::<Vec<_>>())
}
fn pending_transactions_stats(&self) -> Result<BTreeMap<H256, TransactionStats>, Error> {
self.active()?;
let stats = take_weak!(self.sync).transactions_stats();
Ok(stats.into_iter()
.map(|(hash, stats)| (hash.into(), stats.into()))
@ -339,8 +283,6 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
}
fn local_transactions(&self) -> Result<BTreeMap<H256, LocalTransactionStatus>, Error> {
self.active()?;
let transactions = take_weak!(self.miner).local_transactions();
Ok(transactions
.into_iter()
@ -350,8 +292,6 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
}
fn signer_port(&self) -> Result<u16, Error> {
self.active()?;
self.signer
.clone()
.and_then(|signer| signer.address())
@ -360,21 +300,16 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
}
fn dapps_port(&self) -> Result<u16, Error> {
self.active()?;
self.dapps_port
.ok_or_else(|| errors::dapps_disabled())
}
fn dapps_interface(&self) -> Result<String, Error> {
self.active()?;
self.dapps_interface.clone()
.ok_or_else(|| errors::dapps_disabled())
}
fn next_nonce(&self, address: H160) -> Result<U256, Error> {
self.active()?;
let address: Address = address.into();
let miner = take_weak!(self.miner);
let client = take_weak!(self.client);
@ -400,26 +335,21 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
}
fn consensus_capability(&self) -> Result<ConsensusCapability, Error> {
self.active()?;
let updater = take_weak!(self.updater);
Ok(updater.capability().into())
}
fn version_info(&self) -> Result<VersionInfo, Error> {
self.active()?;
let updater = take_weak!(self.updater);
Ok(updater.version_info().into())
}
fn releases_info(&self) -> Result<Option<OperationsInfo>, Error> {
self.active()?;
let updater = take_weak!(self.updater);
Ok(updater.info().map(Into::into))
}
fn chain_status(&self) -> Result<ChainStatus, Error> {
self.active()?;
let chain_info = take_weak!(self.client).chain_info();
let gap = chain_info.ancient_block_number.map(|x| U256::from(x + 1))

View File

@ -21,7 +21,6 @@ use util::{Address};
use ethkey::{Brain, Generator, Secret};
use ethcore::account_provider::AccountProvider;
use ethcore::client::MiningBlockChainClient;
use jsonrpc_core::Error;
use v1::helpers::errors;
@ -29,30 +28,21 @@ use v1::traits::ParityAccounts;
use v1::types::{H160 as RpcH160, H256 as RpcH256, DappId};
/// Account management (personal) rpc implementation.
pub struct ParityAccountsClient<C> where C: MiningBlockChainClient {
pub struct ParityAccountsClient {
accounts: Weak<AccountProvider>,
client: Weak<C>,
}
impl<C> ParityAccountsClient<C> where C: MiningBlockChainClient {
impl ParityAccountsClient {
/// Creates new PersonalClient
pub fn new(store: &Arc<AccountProvider>, client: &Arc<C>) -> Self {
pub fn new(store: &Arc<AccountProvider>) -> Self {
ParityAccountsClient {
accounts: Arc::downgrade(store),
client: Arc::downgrade(client),
}
}
fn active(&self) -> Result<(), Error> {
// TODO: only call every 30s at most.
take_weak!(self.client).keep_alive();
Ok(())
}
}
impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlockChainClient {
impl ParityAccounts for ParityAccountsClient {
fn all_accounts_info(&self) -> Result<BTreeMap<RpcH160, BTreeMap<String, String>>, Error> {
self.active()?;
let store = take_weak!(self.accounts);
let info = store.accounts_info().map_err(|e| errors::account("Could not fetch account info.", e))?;
let other = store.addresses_info();
@ -75,7 +65,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
}
fn new_account_from_phrase(&self, phrase: String, pass: String) -> Result<RpcH160, Error> {
self.active()?;
let store = take_weak!(self.accounts);
let brain = Brain::new(phrase).generate().unwrap();
@ -85,7 +74,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
}
fn new_account_from_wallet(&self, json: String, pass: String) -> Result<RpcH160, Error> {
self.active()?;
let store = take_weak!(self.accounts);
store.import_presale(json.as_bytes(), &pass)
@ -95,7 +83,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
}
fn new_account_from_secret(&self, secret: RpcH256, pass: String) -> Result<RpcH160, Error> {
self.active()?;
let store = take_weak!(self.accounts);
let secret = Secret::from_slice(&secret.0)
@ -106,7 +93,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
}
fn test_password(&self, account: RpcH160, password: String) -> Result<bool, Error> {
self.active()?;
let account: Address = account.into();
take_weak!(self.accounts)
@ -115,7 +101,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
}
fn change_password(&self, account: RpcH160, password: String, new_password: String) -> Result<bool, Error> {
self.active()?;
let account: Address = account.into();
take_weak!(self.accounts)
.change_password(&account, password, new_password)
@ -124,7 +109,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
}
fn kill_account(&self, account: RpcH160, password: String) -> Result<bool, Error> {
self.active()?;
let account: Address = account.into();
take_weak!(self.accounts)
.kill_account(&account, &password)
@ -133,7 +117,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
}
fn remove_address(&self, addr: RpcH160) -> Result<bool, Error> {
self.active()?;
let store = take_weak!(self.accounts);
let addr: Address = addr.into();
@ -142,7 +125,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
}
fn set_account_name(&self, addr: RpcH160, name: String) -> Result<bool, Error> {
self.active()?;
let store = take_weak!(self.accounts);
let addr: Address = addr.into();
@ -152,7 +134,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
}
fn set_account_meta(&self, addr: RpcH160, meta: String) -> Result<bool, Error> {
self.active()?;
let store = take_weak!(self.accounts);
let addr: Address = addr.into();
@ -216,7 +197,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
}
fn geth_accounts(&self) -> Result<Vec<RpcH160>, Error> {
self.active()?;
let store = take_weak!(self.accounts);
Ok(into_vec(store.list_geth_accounts(false)))

View File

@ -23,7 +23,7 @@ use ethcore::client::MiningBlockChainClient;
use ethcore::mode::Mode;
use ethsync::ManageNetwork;
use fetch::{self, Fetch};
use futures::{self, BoxFuture, Future};
use futures::{BoxFuture, Future};
use util::sha3;
use updater::{Service as UpdateService};
@ -62,12 +62,6 @@ impl<C, M, U, F> ParitySetClient<C, M, U, F> where
fetch: fetch,
}
}
fn active(&self) -> Result<(), Error> {
// TODO: only call every 30s at most.
take_weak!(self.client).keep_alive();
Ok(())
}
}
impl<C, M, U, F> ParitySet for ParitySetClient<C, M, U, F> where
@ -78,63 +72,46 @@ impl<C, M, U, F> ParitySet for ParitySetClient<C, M, U, F> where
{
fn set_min_gas_price(&self, gas_price: U256) -> Result<bool, Error> {
self.active()?;
take_weak!(self.miner).set_minimal_gas_price(gas_price.into());
Ok(true)
}
fn set_gas_floor_target(&self, target: U256) -> Result<bool, Error> {
self.active()?;
take_weak!(self.miner).set_gas_floor_target(target.into());
Ok(true)
}
fn set_gas_ceil_target(&self, target: U256) -> Result<bool, Error> {
self.active()?;
take_weak!(self.miner).set_gas_ceil_target(target.into());
Ok(true)
}
fn set_extra_data(&self, extra_data: Bytes) -> Result<bool, Error> {
self.active()?;
take_weak!(self.miner).set_extra_data(extra_data.into_vec());
Ok(true)
}
fn set_author(&self, author: H160) -> Result<bool, Error> {
self.active()?;
take_weak!(self.miner).set_author(author.into());
Ok(true)
}
fn set_engine_signer(&self, address: H160, password: String) -> Result<bool, Error> {
self.active()?;
take_weak!(self.miner).set_engine_signer(address.into(), password).map_err(Into::into).map_err(errors::from_password_error)?;
Ok(true)
}
fn set_transactions_limit(&self, limit: usize) -> Result<bool, Error> {
self.active()?;
take_weak!(self.miner).set_transactions_limit(limit);
Ok(true)
}
fn set_tx_gas_limit(&self, limit: U256) -> Result<bool, Error> {
self.active()?;
take_weak!(self.miner).set_tx_gas_limit(limit.into());
Ok(true)
}
fn add_reserved_peer(&self, peer: String) -> Result<bool, Error> {
self.active()?;
match take_weak!(self.net).add_reserved_peer(peer) {
Ok(()) => Ok(true),
Err(e) => Err(errors::invalid_params("Peer address", e)),
@ -142,8 +119,6 @@ impl<C, M, U, F> ParitySet for ParitySetClient<C, M, U, F> where
}
fn remove_reserved_peer(&self, peer: String) -> Result<bool, Error> {
self.active()?;
match take_weak!(self.net).remove_reserved_peer(peer) {
Ok(()) => Ok(true),
Err(e) => Err(errors::invalid_params("Peer address", e)),
@ -151,15 +126,11 @@ impl<C, M, U, F> ParitySet for ParitySetClient<C, M, U, F> where
}
fn drop_non_reserved_peers(&self) -> Result<bool, Error> {
self.active()?;
take_weak!(self.net).deny_unreserved_peers();
Ok(true)
}
fn accept_non_reserved_peers(&self) -> Result<bool, Error> {
self.active()?;
take_weak!(self.net).accept_unreserved_peers();
Ok(true)
}
@ -186,10 +157,6 @@ impl<C, M, U, F> ParitySet for ParitySetClient<C, M, U, F> where
}
fn hash_content(&self, url: String) -> BoxFuture<H256, Error> {
if let Err(e) = self.active() {
return futures::failed(e).boxed();
}
self.fetch.process(self.fetch.fetch(&url).then(move |result| {
result
.map_err(errors::from_fetch_error)
@ -201,13 +168,11 @@ impl<C, M, U, F> ParitySet for ParitySetClient<C, M, U, F> where
}
fn upgrade_ready(&self) -> Result<Option<ReleaseInfo>, Error> {
self.active()?;
let updater = take_weak!(self.updater);
Ok(updater.upgrade_ready().map(Into::into))
}
fn execute_upgrade(&self) -> Result<bool, Error> {
self.active()?;
let updater = take_weak!(self.updater);
Ok(updater.execute_upgrade())
}

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