Merge branch 'master' into lightrpc
This commit is contained in:
commit
7c9064c856
53
Cargo.lock
generated
53
Cargo.lock
generated
@ -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>"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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";
|
||||
|
@ -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,
|
||||
|
@ -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()]);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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\
|
||||
{}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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>;
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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());
|
||||
|
@ -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':
|
||||
|
@ -20,7 +20,6 @@
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: 1.5em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
@ -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 } />
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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 });
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 }
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
61
js/src/ui/Form/DappUrlInput/dappUrlInput.js
Normal file
61
js/src/ui/Form/DappUrlInput/dappUrlInput.js
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
70
js/src/ui/Form/DappUrlInput/dappUrlInput.spec.js
Normal file
70
js/src/ui/Form/DappUrlInput/dappUrlInput.spec.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
17
js/src/ui/Form/DappUrlInput/index.js
Normal file
17
js/src/ui/Form/DappUrlInput/index.js
Normal 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';
|
@ -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 }
|
||||
|
17
js/src/ui/Form/InputDate/index.js
Normal file
17
js/src/ui/Form/InputDate/index.js
Normal 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';
|
22
js/src/ui/Form/InputDate/inputDate.css
Normal file
22
js/src/ui/Form/InputDate/inputDate.css
Normal 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%;
|
||||
}
|
||||
}
|
53
js/src/ui/Form/InputDate/inputDate.js
Normal file
53
js/src/ui/Form/InputDate/inputDate.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
17
js/src/ui/Form/InputTime/index.js
Normal file
17
js/src/ui/Form/InputTime/index.js
Normal 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';
|
22
js/src/ui/Form/InputTime/inputTime.css
Normal file
22
js/src/ui/Form/InputTime/inputTime.css
Normal 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%;
|
||||
}
|
||||
}
|
54
js/src/ui/Form/InputTime/inputTime.js
Normal file
54
js/src/ui/Form/InputTime/inputTime.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
17
js/src/ui/Form/Label/index.js
Normal file
17
js/src/ui/Form/Label/index.js
Normal 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';
|
24
js/src/ui/Form/Label/label.css
Normal file
24
js/src/ui/Form/Label/label.css
Normal 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;
|
||||
}
|
40
js/src/ui/Form/Label/label.js
Normal file
40
js/src/ui/Form/Label/label.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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 () {
|
||||
|
@ -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 {
|
||||
|
@ -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}`;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
26
js/src/ui/Title/title.css
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
@ -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
60
js/src/util/dapplink.js
Normal 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
|
||||
};
|
83
js/src/util/dapplink.spec.js
Normal file
83
js/src/util/dapplink.spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
52
js/src/views/Application/Extension/extension.css
Normal file
52
js/src/views/Application/Extension/extension.css
Normal 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;
|
||||
}
|
||||
}
|
74
js/src/views/Application/Extension/extension.js
Normal file
74
js/src/views/Application/Extension/extension.js
Normal 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();
|
||||
}
|
||||
}
|
17
js/src/views/Application/Extension/index.js
Normal file
17
js/src/views/Application/Extension/index.js
Normal 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';
|
89
js/src/views/Application/Extension/store.js
Normal file
89
js/src/views/Application/Extension/store.js
Normal 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');
|
||||
});
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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 = () => {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
48
js/src/views/Web/AddressBar/addressBar.spec.js
Normal file
48
js/src/views/Web/AddressBar/addressBar.spec.js
Normal 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
158
js/src/views/Web/store.js
Normal 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
|
||||
};
|
202
js/src/views/Web/store.spec.js
Normal file
202
js/src/views/Web/store.spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
56
js/src/views/Web/web.spec.js
Normal file
56
js/src/views/Web/web.spec.js
Normal 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');
|
||||
});
|
||||
});
|
@ -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| {
|
||||
|
@ -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 => {
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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(
|
||||
|
@ -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())?;
|
||||
|
@ -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)
|
||||
};
|
||||
|
@ -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" }
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
)
|
||||
|
301
rpc/src/v1/helpers/informant.rs
Normal file
301
rpc/src/v1/helpers/informant.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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)))
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user