Merge branch 'master' into lightrpc

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

53
Cargo.lock generated
View File

@ -28,7 +28,7 @@ dependencies = [
"fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "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)", "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)", "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)", "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]] [[package]]
name = "bigint" name = "bigint"
version = "1.0.0" version = "1.0.0"
@ -414,6 +419,7 @@ dependencies = [
name = "ethcore-dapps" name = "ethcore-dapps"
version = "1.6.0" version = "1.6.0"
dependencies = [ 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)", "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)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"ethcore-devtools 1.6.0", "ethcore-devtools 1.6.0",
@ -422,7 +428,7 @@ dependencies = [
"fetch 0.1.0", "fetch 0.1.0",
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "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)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
@ -595,11 +601,12 @@ dependencies = [
"ethsync 1.6.0", "ethsync 1.6.0",
"fetch 0.1.0", "fetch 0.1.0",
"futures 0.1.6 (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)",
"jsonrpc-http-server 7.0.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-ipc-server 1.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-macros 0.2.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)", "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-reactor 0.1.0",
"parity-updater 1.6.0", "parity-updater 1.6.0",
"rlp 0.1.0", "rlp 0.1.0",
@ -622,7 +629,7 @@ dependencies = [
"ethcore-io 1.6.0", "ethcore-io 1.6.0",
"ethcore-rpc 1.6.0", "ethcore-rpc 1.6.0",
"ethcore-util 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)", "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-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-ui 1.6.0", "parity-ui 1.6.0",
@ -642,7 +649,7 @@ dependencies = [
"ethcore-ipc-nano 1.6.0", "ethcore-ipc-nano 1.6.0",
"ethcore-util 1.6.0", "ethcore-util 1.6.0",
"futures 0.1.6 (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)",
"jsonrpc-macros 0.2.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)", "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)", "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]] [[package]]
name = "jsonrpc-core" name = "jsonrpc-core"
version = "5.0.0" version = "5.1.0"
source = "git+https://github.com/ethcore/jsonrpc.git#5eeee0980e4d2682a831c633fa03a8af99e0d68c" source = "git+https://github.com/ethcore/jsonrpc.git#d179ce34d8da8ea1cd67e93a5b4cb1e30f48c156"
dependencies = [ dependencies = [
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1019,11 +1026,10 @@ dependencies = [
[[package]] [[package]]
name = "jsonrpc-http-server" name = "jsonrpc-http-server"
version = "7.0.0" version = "7.0.0"
source = "git+https://github.com/ethcore/jsonrpc.git#5eeee0980e4d2682a831c633fa03a8af99e0d68c" source = "git+https://github.com/ethcore/jsonrpc.git#d179ce34d8da8ea1cd67e93a5b4cb1e30f48c156"
dependencies = [ 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)", "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)", "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)", "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)", "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1032,11 +1038,11 @@ dependencies = [
[[package]] [[package]]
name = "jsonrpc-ipc-server" name = "jsonrpc-ipc-server"
version = "1.0.0" version = "1.0.0"
source = "git+https://github.com/ethcore/jsonrpc.git#5eeee0980e4d2682a831c633fa03a8af99e0d68c" source = "git+https://github.com/ethcore/jsonrpc.git#d179ce34d8da8ea1cd67e93a5b4cb1e30f48c156"
dependencies = [ dependencies = [
"bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "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)", "mio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1048,21 +1054,19 @@ dependencies = [
[[package]] [[package]]
name = "jsonrpc-macros" name = "jsonrpc-macros"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/ethcore/jsonrpc.git#5eeee0980e4d2682a831c633fa03a8af99e0d68c" source = "git+https://github.com/ethcore/jsonrpc.git#d179ce34d8da8ea1cd67e93a5b4cb1e30f48c156"
dependencies = [ dependencies = [
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
name = "jsonrpc-tcp-server" name = "jsonrpc-tcp-server"
version = "1.0.0" version = "1.0.0"
source = "git+https://github.com/ethcore/jsonrpc.git#5eeee0980e4d2682a831c633fa03a8af99e0d68c" source = "git+https://github.com/ethcore/jsonrpc.git#d179ce34d8da8ea1cd67e93a5b4cb1e30f48c156"
dependencies = [ dependencies = [
"env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "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.1.0 (git+https://github.com/ethcore/jsonrpc.git)",
"jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "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]] [[package]]
name = "owning_ref" name = "owning_ref"
version = "0.2.2" version = "0.2.2"
@ -1532,7 +1541,7 @@ dependencies = [
"ethcore-signer 1.6.0", "ethcore-signer 1.6.0",
"ethcore-util 1.6.0", "ethcore-util 1.6.0",
"futures 0.1.6 (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)", "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)", "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)", "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1563,7 +1572,7 @@ dependencies = [
[[package]] [[package]]
name = "parity-ui-precompiled" name = "parity-ui-precompiled"
version = "1.4.0" 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 = [ dependencies = [
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "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 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 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 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 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-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" "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 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.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 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-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-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>" "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 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 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 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 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-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "98378dec0a185da2b7180308752f0bad73aaa949c3e0a3b0528d0e067945f7ab"
"checksum parity-ui-precompiled 1.4.0 (git+https://github.com/ethcore/js-precompiled.git)" = "<none>" "checksum parity-ui-precompiled 1.4.0 (git+https://github.com/ethcore/js-precompiled.git)" = "<none>"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -143,7 +143,7 @@ fn should_return_signer_port_cors_headers_for_home_parity() {
"\ "\
POST /api/ping HTTP/1.1\r\n\ POST /api/ping HTTP/1.1\r\n\
Host: localhost:8080\r\n\ Host: localhost:8080\r\n\
Origin: http://home.parity\r\n\ Origin: http://parity.web3.site\r\n\
Connection: close\r\n\ Connection: close\r\n\
\r\n\ \r\n\
{} {}
@ -153,8 +153,8 @@ fn should_return_signer_port_cors_headers_for_home_parity() {
// then // then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert!( assert!(
response.headers_raw.contains("Access-Control-Allow-Origin: http://home.parity"), response.headers_raw.contains("Access-Control-Allow-Origin: http://parity.web3.site"),
"CORS header for home.parity missing: {:?}", "CORS header for parity.web3.site missing: {:?}",
response.headers response.headers
); );
} }

View File

@ -31,7 +31,7 @@ fn should_resolve_dapp() {
let response = request(server, let response = request(server,
"\ "\
GET / HTTP/1.1\r\n\ GET / HTTP/1.1\r\n\
Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.parity\r\n\ Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.web3.site\r\n\
Connection: close\r\n\ Connection: close\r\n\
\r\n\ \r\n\
" "
@ -52,7 +52,7 @@ fn should_return_503_when_syncing_but_should_make_the_calls() {
let response = request(server, let response = request(server,
"\ "\
GET / HTTP/1.1\r\n\ GET / HTTP/1.1\r\n\
Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.parity\r\n\ Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.web3.site\r\n\
Connection: close\r\n\ Connection: close\r\n\
\r\n\ \r\n\
" "
@ -81,7 +81,7 @@ fn should_return_502_on_hash_mismatch() {
let response = request(server, let response = request(server,
"\ "\
GET / HTTP/1.1\r\n\ GET / HTTP/1.1\r\n\
Host: 94f093625c06887d94d9fee0d5f9cc4aaa46f33d24d1c7e4b5237e7c37d547dd.parity\r\n\ Host: 94f093625c06887d94d9fee0d5f9cc4aaa46f33d24d1c7e4b5237e7c37d547dd.web3.site\r\n\
Connection: close\r\n\ Connection: close\r\n\
\r\n\ \r\n\
" "
@ -112,7 +112,7 @@ fn should_return_error_for_invalid_dapp_zip() {
let response = request(server, let response = request(server,
"\ "\
GET / HTTP/1.1\r\n\ GET / HTTP/1.1\r\n\
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\ Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.web3.site\r\n\
Connection: close\r\n\ Connection: close\r\n\
\r\n\ \r\n\
" "
@ -144,7 +144,7 @@ fn should_return_fetched_dapp_content() {
let response1 = http_client::request(server.addr(), let response1 = http_client::request(server.addr(),
"\ "\
GET /index.html HTTP/1.1\r\n\ GET /index.html HTTP/1.1\r\n\
Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.parity\r\n\ Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.web3.site\r\n\
Connection: close\r\n\ Connection: close\r\n\
\r\n\ \r\n\
" "
@ -152,7 +152,7 @@ fn should_return_fetched_dapp_content() {
let response2 = http_client::request(server.addr(), let response2 = http_client::request(server.addr(),
"\ "\
GET /manifest.json HTTP/1.1\r\n\ GET /manifest.json HTTP/1.1\r\n\
Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.parity\r\n\ Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.web3.site\r\n\
Connection: close\r\n\ Connection: close\r\n\
\r\n\ \r\n\
" "
@ -207,7 +207,7 @@ fn should_return_fetched_content() {
let response = request(server, let response = request(server,
"\ "\
GET / HTTP/1.1\r\n\ GET / HTTP/1.1\r\n\
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\ Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.web3.site\r\n\
Connection: close\r\n\ Connection: close\r\n\
\r\n\ \r\n\
" "
@ -234,7 +234,7 @@ fn should_cache_content() {
); );
let request_str = "\ let request_str = "\
GET / HTTP/1.1\r\n\ GET / HTTP/1.1\r\n\
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\ Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.web3.site\r\n\
Connection: close\r\n\ Connection: close\r\n\
\r\n\ \r\n\
"; ";
@ -265,7 +265,7 @@ fn should_not_request_content_twice() {
); );
let request_str = "\ let request_str = "\
GET / HTTP/1.1\r\n\ GET / HTTP/1.1\r\n\
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\ Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.web3.site\r\n\
Connection: close\r\n\ Connection: close\r\n\
\r\n\ \r\n\
"; ";
@ -298,6 +298,17 @@ fn should_not_request_content_twice() {
response2.assert_status("HTTP/1.1 200 OK"); 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] #[test]
fn should_stream_web_content() { fn should_stream_web_content() {
// given // given
@ -306,8 +317,8 @@ fn should_stream_web_content() {
// when // when
let response = request(server, let response = request(server,
"\ "\
GET /web/token/https/parity.io/ HTTP/1.1\r\n\ GET / HTTP/1.1\r\n\
Host: localhost:8080\r\n\ Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\
Connection: close\r\n\ Connection: close\r\n\
\r\n\ \r\n\
" "
@ -322,20 +333,90 @@ fn should_stream_web_content() {
} }
#[test] #[test]
fn should_return_error_on_invalid_token() { fn should_support_base32_encoded_web_urls() {
// given // given
let (server, fetch) = serve_with_fetch("token"); let (server, fetch) = serve_with_fetch("token");
// when // when
let response = request(server, 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\ Host: localhost:8080\r\n\
Connection: close\r\n\ Connection: close\r\n\
\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 // then
response.assert_status("HTTP/1.1 400 Bad Request"); response.assert_status("HTTP/1.1 400 Bad Request");
assert_security_headers_for_embed(&response.headers); assert_security_headers_for_embed(&response.headers);
@ -365,28 +446,6 @@ fn should_return_error_on_invalid_protocol() {
fetch.assert_no_more_requests(); 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] #[test]
fn should_disallow_non_get_requests() { fn should_disallow_non_get_requests() {
// given // given
@ -395,8 +454,8 @@ fn should_disallow_non_get_requests() {
// when // when
let response = request(server, let response = request(server,
"\ "\
POST /token/https/parity.io/ HTTP/1.1\r\n\ POST / HTTP/1.1\r\n\
Host: web.parity\r\n\ Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\
Content-Type: application/json\r\n\ Content-Type: application/json\r\n\
Connection: close\r\n\ Connection: close\r\n\
\r\n\ \r\n\
@ -423,14 +482,37 @@ fn should_fix_absolute_requests_based_on_referer() {
GET /styles.css HTTP/1.1\r\n\ GET /styles.css HTTP/1.1\r\n\
Host: localhost:8080\r\n\ Host: localhost:8080\r\n\
Connection: close\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\ \r\n\
" "
); );
// then // then
response.assert_status("HTTP/1.1 302 Found"); 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(); fetch.assert_no_more_requests();
} }

View File

@ -105,7 +105,7 @@ fn should_display_404_on_invalid_dapp_with_domain() {
let response = request(server, let response = request(server,
"\ "\
GET / HTTP/1.1\r\n\ GET / HTTP/1.1\r\n\
Host: invaliddapp.parity\r\n\ Host: invaliddapp.web3.site\r\n\
Connection: close\r\n\ Connection: close\r\n\
\r\n\ \r\n\
" "
@ -179,7 +179,7 @@ fn should_serve_proxy_pac() {
// then // then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); 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); assert_security_headers(&response.headers);
} }

View File

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

View File

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

View File

@ -20,6 +20,7 @@ use std::sync::Arc;
use fetch::{self, Fetch}; use fetch::{self, Fetch};
use parity_reactor::Remote; use parity_reactor::Remote;
use base32;
use hyper::{self, server, net, Next, Encoder, Decoder}; use hyper::{self, server, net, Next, Encoder, Decoder};
use hyper::status::StatusCode; use hyper::status::StatusCode;
@ -27,7 +28,7 @@ use apps;
use endpoint::{Endpoint, Handler, EndpointPath}; use endpoint::{Endpoint, Handler, EndpointPath};
use handlers::{ use handlers::{
ContentFetcherHandler, ContentHandler, ContentValidator, ValidatorResponse, ContentFetcherHandler, ContentHandler, ContentValidator, ValidatorResponse,
StreamingHandler, Redirection, extract_url, StreamingHandler, extract_url,
}; };
use url::Url; use url::Url;
use WebProxyTokens; use WebProxyTokens;
@ -86,9 +87,10 @@ impl ContentValidator for WebInstaller {
); );
if is_html { if is_html {
handler.set_initial_content(&format!( 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::UTILS_PATH,
apps::URL_REFERER, apps::URL_REFERER,
apps::WEB_PATH,
&self.referer, &self.referer,
)); ));
} }
@ -99,7 +101,6 @@ impl ContentValidator for WebInstaller {
enum State<F: Fetch> { enum State<F: Fetch> {
Initial, Initial,
Error(ContentHandler), Error(ContentHandler),
Redirecting(Redirection),
Fetching(ContentFetcherHandler<WebInstaller, F>), Fetching(ContentFetcherHandler<WebInstaller, F>),
} }
@ -114,25 +115,26 @@ struct WebHandler<F: Fetch> {
} }
impl<F: Fetch> WebHandler<F> { impl<F: Fetch> WebHandler<F> {
fn extract_target_url(&self, url: Option<Url>) -> Result<(String, String), State<F>> { fn extract_target_url(&self, url: Option<Url>) -> Result<String, State<F>> {
let (path, query) = match url { let token_and_url = self.path.app_params.get(0)
Some(url) => (url.path, url.query), .map(|encoded| encoded.replace('.', ""))
None => { .and_then(|encoded| base32::decode(base32::Alphabet::Crockford, &encoded.to_uppercase()))
return Err(State::Error(ContentHandler::error( .and_then(|data| String::from_utf8(data).ok())
StatusCode::BadRequest, "Invalid URL", "Couldn't parse URL", None, self.embeddable_on.clone() .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 mut token_it = token_and_url.split('+');
let idx = match path.get(0).map(|m| m.as_ref()) { let token = token_it.next();
Some(apps::WEB_PATH) => 1, let target_url = token_it.next();
_ => 0,
};
// Check if token supplied in URL is correct. // Check if token supplied in URL is correct.
match path.get(idx) { match token {
Some(ref token) if self.web_proxy_tokens.is_web_proxy_token_valid(token) => {}, Some(token) if self.web_proxy_tokens.is_web_proxy_token_valid(token) => {},
_ => { _ => {
return Err(State::Error(ContentHandler::error( 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() 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 // Validate protocol
let protocol = match path.get(idx + 1).map(|a| a.as_str()) { let mut target_url = match target_url {
Some("http") => "http", Some(url) if url.starts_with("http://") || url.starts_with("https://") => url.to_owned(),
Some("https") => "https",
_ => { _ => {
return Err(State::Error(ContentHandler::error( return Err(State::Error(ContentHandler::error(
StatusCode::BadRequest, "Invalid Protocol", "Invalid protocol used.", None, self.embeddable_on.clone() 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 !target_url.ends_with("/") {
if let None = path.get(idx + 3) { target_url = format!("{}/", target_url);
return Err(State::Redirecting(
Redirection::new(&format!("/{}/", path.join("/")))
));
} }
let query = match query { // TODO [ToDr] Should just use `path.app_params`
Some(query) => format!("?{}", query), 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(), 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> { impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> {
fn on_request(&mut self, request: server::Request<net::HttpStream>) -> Next { fn on_request(&mut self, request: server::Request<net::HttpStream>) -> Next {
let url = extract_url(&request); let url = extract_url(&request);
// First extract the URL (reject invalid URLs) // 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, Ok(url) => url,
Err(error) => { Err(error) => {
self.state = error; self.state = error;
@ -186,7 +194,9 @@ impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> {
self.control.clone(), self.control.clone(),
WebInstaller { WebInstaller {
embeddable_on: self.embeddable_on.clone(), 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.embeddable_on.clone(),
self.remote.clone(), self.remote.clone(),
@ -202,7 +212,6 @@ impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> {
match self.state { match self.state {
State::Initial => Next::end(), State::Initial => Next::end(),
State::Error(ref mut handler) => handler.on_request_readable(decoder), 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), 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 { match self.state {
State::Initial => Next::end(), State::Initial => Next::end(),
State::Error(ref mut handler) => handler.on_response(res), 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), 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 { match self.state {
State::Initial => Next::end(), State::Initial => Next::end(),
State::Error(ref mut handler) => handler.on_response_writable(encoder), 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), State::Fetching(ref mut handler) => handler.on_response_writable(encoder),
} }
} }

View File

@ -87,7 +87,7 @@ pub fn request(address: &SocketAddr, request: &str) -> Response {
let _ = req.read_to_string(&mut response); let _ = req.read_to_string(&mut response);
let mut lines = response.lines(); 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_raw = read_block(&mut lines, false);
let headers = headers_raw.split('\n').map(|v| v.to_owned()).collect(); let headers = headers_raw.split('\n').map(|v| v.to_owned()).collect();
let body = read_block(&mut lines, true); let body = read_block(&mut lines, true);

View File

@ -262,6 +262,18 @@ impl Client {
Ok(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 /// Adds an actor to be notified on certain events
pub fn add_notify(&self, target: Arc<ChainNotify>) { pub fn add_notify(&self, target: Arc<ChainNotify>) {
self.notify.write().push(Arc::downgrade(&target)); self.notify.write().push(Arc::downgrade(&target));
@ -1011,17 +1023,6 @@ impl BlockChainClient for Client {
Ok(ret) 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 { fn mode(&self) -> IpcMode {
let r = self.mode.lock().clone().into(); let r = self.mode.lock().clone().into();
trace!(target: "mode", "Asked for mode = {:?}. returning {:?}", &*self.mode.lock(), r); trace!(target: "mode", "Asked for mode = {:?}. returning {:?}", &*self.mode.lock(), r);

View File

@ -46,10 +46,6 @@ use encoded;
/// Blockchain database client. Owns and manages a blockchain and a block queue. /// Blockchain database client. Owns and manages a blockchain and a block queue.
pub trait BlockChainClient : Sync + Send { 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. /// Get raw block header data by block id.
fn block_header(&self, id: BlockId) -> Option<encoded::Header>; fn block_header(&self, id: BlockId) -> Option<encoded::Header>;

View File

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

View File

@ -127,6 +127,18 @@ export function inNumber16 (number) {
return inHex(bn.toString(16)); 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) { export function inOptions (options) {
if (options) { if (options) {
Object.keys(options).forEach((key) => { Object.keys(options).forEach((key) => {
@ -136,6 +148,10 @@ export function inOptions (options) {
options[key] = inAddress(options[key]); options[key] = inAddress(options[key]);
break; break;
case 'condition':
options[key] = inOptionsCondition(options[key]);
break;
case 'gas': case 'gas':
case 'gasPrice': case 'gasPrice':
options[key] = inNumber16((new BigNumber(options[key])).round()); options[key] = inNumber16((new BigNumber(options[key])).round());

View File

@ -221,6 +221,18 @@ export function outSyncing (syncing) {
return 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) { export function outTransaction (tx) {
if (tx) { if (tx) {
Object.keys(tx).forEach((key) => { Object.keys(tx).forEach((key) => {
@ -234,8 +246,14 @@ export function outTransaction (tx) {
tx[key] = outNumber(tx[key]); tx[key] = outNumber(tx[key]);
break; break;
case 'condition':
tx[key] = outTransactionCondition(tx[key]);
break;
case 'minBlock': case 'minBlock':
tx[key] = tx[key] ? outNumber(tx[key]) : null; tx[key] = tx[key]
? outNumber(tx[key])
: null;
break; break;
case 'creates': case 'creates':

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; 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 { CheckIcon, StarIcon, StarOutlineIcon } from '~/ui/Icons';
import styles from './dappPermissions.css'; import styles from './dappPermissions.css';
@ -38,18 +38,27 @@ export default class DappPermissions extends Component {
return ( return (
<Portal <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 } onClose={ store.closeModal }
open 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 }> <div className={ styles.container }>
<SectionList <SectionList
items={ store.accounts } items={ store.accounts }
@ -57,16 +66,6 @@ export default class DappPermissions extends Component {
renderItem={ this.renderAccount } renderItem={ this.renderAccount }
/> />
</div> </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> </Portal>
); );
} }

View File

@ -15,45 +15,22 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; 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'; import styles from '../executeContract.css';
export default class AdvancedStep extends Component { export default class AdvancedStep extends Component {
static propTypes = { static propTypes = {
gasStore: PropTypes.object.isRequired, gasStore: PropTypes.object.isRequired
minBlock: PropTypes.string,
minBlockError: PropTypes.string,
onMinBlockChange: PropTypes.func
}; };
render () { render () {
const { gasStore, minBlock, minBlockError, onMinBlockChange } = this.props; const { gasStore } = this.props;
return ( return (
<div> <div className={ styles.gaseditor }>
<Input <GasPriceEditor store={ gasStore } />
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> </div>
); );
} }

View File

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

View File

@ -27,36 +27,17 @@ export default class Extras extends Component {
dataError: PropTypes.string, dataError: PropTypes.string,
gasStore: PropTypes.object.isRequired, gasStore: PropTypes.object.isRequired,
isEth: PropTypes.bool, isEth: PropTypes.bool,
minBlock: PropTypes.string,
minBlockError: PropTypes.string,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
total: PropTypes.string, total: PropTypes.string,
totalError: PropTypes.string totalError: PropTypes.string
} }
render () { render () {
const { gasStore, minBlock, minBlockError, onChange } = this.props; const { gasStore, onChange } = this.props;
return ( return (
<Form> <Form>
{ this.renderData() } { 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 }> <div className={ styles.gaseditor }>
<GasPriceEditor <GasPriceEditor
store={ gasStore } store={ gasStore }
@ -98,8 +79,4 @@ export default class Extras extends Component {
onEditData = (event) => { onEditData = (event) => {
this.props.onChange('data', event.target.value); this.props.onChange('data', event.target.value);
} }
onEditMinBlock = (event) => {
this.props.onChange('minBlock', event.target.value);
}
} }

View File

@ -52,9 +52,6 @@ export default class TransferStore {
@observable data = ''; @observable data = '';
@observable dataError = null; @observable dataError = null;
@observable minBlock = '0';
@observable minBlockError = null;
@observable recipient = ''; @observable recipient = '';
@observable recipientError = ERRORS.requireRecipient; @observable recipientError = ERRORS.requireRecipient;
@ -78,39 +75,6 @@ export default class TransferStore {
gasStore = null; 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) { constructor (api, props) {
this.api = api; 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 = () => { @action onNext = () => {
this.stage += 1; this.stage += 1;
} }
@ -164,9 +161,6 @@ export default class TransferStore {
case 'gasPrice': case 'gasPrice':
return this._onUpdateGasPrice(value); return this._onUpdateGasPrice(value);
case 'minBlock':
return this._onUpdateMinBlock(value);
case 'recipient': case 'recipient':
return this._onUpdateRecipient(value); return this._onUpdateRecipient(value);
@ -284,14 +278,6 @@ export default class TransferStore {
this.recalculate(); this.recalculate();
} }
@action _onUpdateMinBlock = (minBlock) => {
console.log('minBlock', minBlock);
transaction(() => {
this.minBlock = minBlock;
this.minBlockError = this._validatePositiveNumber(minBlock);
});
}
@action _onUpdateGasPrice = (gasPrice) => { @action _onUpdateGasPrice = (gasPrice) => {
this.recalculate(); this.recalculate();
} }
@ -590,7 +576,6 @@ export default class TransferStore {
send () { send () {
const { options, values } = this._getTransferParams(); 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()); log.debug('@send', 'transfer value', options.value && options.value.toFormat());
return this._getTransferMethod().postTransaction(options, values); return this._getTransferMethod().postTransaction(options, values);
@ -639,15 +624,12 @@ export default class TransferStore {
const to = (isEth && !isWallet) ? this.recipient const to = (isEth && !isWallet) ? this.recipient
: (this.isWallet ? this.wallet.address : this.token.address); : (this.isWallet ? this.wallet.address : this.token.address);
const options = { const options = this.gasStore.overrideTransaction({
from: this.sender || this.account.address, from: this.sender || this.account.address,
to to
}; });
if (!gas) { if (gas) {
options.gas = this.gasStore.gas;
options.gasPrice = this.gasStore.price;
} else {
options.gas = MAX_GAS_ESTIMATION; options.gas = MAX_GAS_ESTIMATION;
} }

View File

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

View File

@ -52,13 +52,11 @@ export default class SignerMiddleware {
} }
onConfirmStart = (store, action) => { 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) => { const handlePromise = (promise) => {
promise promise
.then((txHash) => { .then((txHash) => {
console.log('confirmRequest', id, txHash);
if (!txHash) { if (!txHash) {
store.dispatch(actions.errorConfirmRequest({ id, err: 'Unable to confirm.' })); store.dispatch(actions.errorConfirmRequest({ id, err: 'Unable to confirm.' }));
return; 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) => { onRejectStart = (store, action) => {

View File

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

View File

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

View File

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

View File

@ -180,34 +180,38 @@ class AddressSelect extends Component {
onClose={ this.handleClose } onClose={ this.handleClose }
onKeyDown={ this.handleKeyDown } onKeyDown={ this.handleKeyDown }
open={ expanded } 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() } { this.renderAccounts() }
</Portal> </Portal>
); );

View File

@ -0,0 +1,61 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import keycode from 'keycode';
import React, { Component, PropTypes } from 'react';
export default class DappUrlInput extends Component {
static propTypes = {
className: PropTypes.string,
onChange: PropTypes.func.isRequired,
onGoto: PropTypes.func.isRequired,
onRestore: PropTypes.func.isRequired,
url: PropTypes.string.isRequired
}
render () {
const { className, url } = this.props;
return (
<input
className={ className }
onChange={ this.onChange }
onKeyDown={ this.onKeyDown }
type='text'
value={ url }
/>
);
}
onChange = (event) => {
this.props.onChange(event.target.value);
};
onKeyDown = (event) => {
switch (keycode(event)) {
case 'esc':
this.props.onRestore();
break;
case 'enter':
this.props.onGoto();
break;
default:
break;
}
};
}

View File

@ -0,0 +1,70 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import DappUrlInput from './';
let component;
let onChange;
let onGoto;
let onRestore;
function render (props = { url: 'http://some.url' }) {
onChange = sinon.stub();
onGoto = sinon.stub();
onRestore = sinon.stub();
component = shallow(
<DappUrlInput
onChange={ onChange }
onGoto={ onGoto }
onRestore={ onRestore }
{ ...props }
/>
);
return component;
}
describe('ui/Form/DappUrlInput', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
describe('events', () => {
describe('onChange', () => {
it('calls the onChange callback as provided', () => {
component.simulate('change', { target: { value: 'testing' } });
expect(onChange).to.have.been.calledWith('testing');
});
});
describe('onKeyDown', () => {
it('calls the onGoto callback on enter', () => {
component.simulate('keyDown', { keyCode: 13 });
expect(onGoto).to.have.been.called;
});
it('calls the onRestor callback on esc', () => {
component.simulate('keyDown', { keyCode: 27 });
expect(onRestore).to.have.been.called;
});
});
});
});

View File

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

View File

@ -78,6 +78,9 @@ class InputAddress extends Component {
props.focused = focused; 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 ( return (
<div className={ containerClasses.join(' ') }> <div className={ containerClasses.join(' ') }>
<Input <Input
@ -96,7 +99,7 @@ class InputAddress extends Component {
tabIndex={ tabIndex } tabIndex={ tabIndex }
value={ value={
text && account text && account
? account.name ? (account.name || (value ? 'UNNAMED' : value))
: (nullName || value) : (nullName || value)
} }
{ ...props } { ...props }

View File

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

View File

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

View File

@ -0,0 +1,53 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { DatePicker } from 'material-ui';
import React, { Component, PropTypes } from 'react';
import Label from '../Label';
import styles from './inputDate.css';
// NOTE: Has to be larger than Signer overlay Z, aligns with ../InputTime
const DIALOG_STYLE = { zIndex: 10010 };
export default class InputDate extends Component {
static propTypes = {
className: PropTypes.string,
hint: PropTypes.node,
label: PropTypes.node,
onChange: PropTypes.func,
value: PropTypes.object.isRequired
};
render () {
const { className, hint, label, onChange, value } = this.props;
return (
<div className={ [styles.container, className].join(' ') }>
<Label label={ label } />
<DatePicker
autoOk
className={ styles.input }
dialogContainerStyle={ DIALOG_STYLE }
hintText={ hint }
onChange={ onChange }
value={ value }
/>
</div>
);
}
}

View File

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

View File

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

View File

@ -0,0 +1,54 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { TimePicker } from 'material-ui';
import React, { Component, PropTypes } from 'react';
import Label from '../Label';
import styles from './inputTime.css';
// NOTE: Has to be larger than Signer overlay Z, aligns with ../InputDate
const DIALOG_STYLE = { zIndex: 10010 };
export default class InputTime extends Component {
static propTypes = {
className: PropTypes.string,
hint: PropTypes.node,
label: PropTypes.node,
onChange: PropTypes.func,
value: PropTypes.object.isRequired
}
render () {
const { className, hint, label, onChange, value } = this.props;
return (
<div className={ [styles.container, className].join(' ') }>
<Label label={ label } />
<TimePicker
autoOk
className={ styles.input }
dialogStyle={ DIALOG_STYLE }
format='24hr'
hintText={ hint }
onChange={ onChange }
value={ value }
/>
</div>
);
}
}

View File

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

View File

@ -0,0 +1,24 @@
/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
$labelColor: rgba(255, 255, 255, 0.5);
$labelFontSize: 0.75rem;
.label {
color: $labelColor;
font-size: $labelFontSize;
}

View File

@ -0,0 +1,40 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import styles from './label.css';
export default class Label extends Component {
static propTypes = {
className: PropTypes.string,
label: PropTypes.node
}
render () {
const { className, label } = this.props;
if (!label) {
return null;
}
return (
<label className={ [styles.label, className].join(' ') }>
{ label }
</label>
);
}
}

View File

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

View File

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

View File

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

View File

@ -16,6 +16,46 @@
*/ */
.container { .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; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
position: relative; position: relative;

View File

@ -17,13 +17,44 @@
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React, { Component, PropTypes } from '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 GasPriceSelector from '../GasPriceSelector';
import Store from './store';
import Store, { CONDITIONS } from './store';
import styles from './gasPriceEditor.css'; 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 @observer
export default class GasPriceEditor extends Component { export default class GasPriceEditor extends Component {
static contextTypes = { static contextTypes = {
@ -41,7 +72,7 @@ export default class GasPriceEditor extends Component {
render () { render () {
const { api } = this.context; const { api } = this.context;
const { children, store } = this.props; 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 eth = api.util.fromWei(totalValue).toFormat();
const gasLabel = `gas (estimated: ${new BigNumber(estimated).toFormat()})`; const gasLabel = `gas (estimated: ${new BigNumber(estimated).toFormat()})`;
@ -49,46 +80,146 @@ export default class GasPriceEditor extends Component {
return ( return (
<div className={ styles.container }> <div className={ styles.container }>
<div className={ styles.graphColumn }> <RadioButtons
<GasPriceSelector className={ styles.conditionRadio }
histogram={ histogram } label={
onChange={ this.onEditGasPrice } <FormattedMessage
price={ price } id='txEditor.condition.label'
/> defaultMessage='Condition where transaction activates'
<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. }
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>
</div>
);
}
<div className={ styles.editColumn }> renderConditions () {
<div className={ styles.row }> 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 <Input
error={ errorGas } error={ conditionBlockError }
hint='the amount of gas to use for the transaction' hint={
label={ gasLabel } <FormattedMessage
onChange={ this.onEditGas } id='txEditor.condition.block.hint'
value={ gas } defaultMessage='The minimum block to send from'
/> />
<Input }
error={ errorPrice } label={
hint='the price of gas to use for the transaction' <FormattedMessage
label={ priceLabel } id='txEditor.condition.block.label'
onChange={ this.onEditGasPrice } defaultMessage='Transaction send block'
value={ price } />
}
min={ 1 }
onChange={ this.onChangeConditionBlock }
type='number'
value={ condition.block }
/> />
</div> </div>
<div className={ styles.row }> </div>
<Input );
disabled }
error={ errorTotal }
hint='the total amount of the transaction' return (
label='total transaction amount' <div className={ styles.conditionContainer }>
value={ `${eth} ETH` } <div className={ styles.input }>
/> <InputDate
</div> hint={
<div className={ styles.row }> <FormattedMessage
{ children } id='txEditor.condition.date.hint'
</div> 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>
</div> </div>
); );
@ -107,4 +238,16 @@ export default class GasPriceEditor extends Component {
store.setPrice(price); store.setPrice(price);
onChange && onChange('gasPrice', price); onChange && onChange('gasPrice', price);
} }
onChangeConditionType = (conditionType) => {
this.props.store.setConditionType(conditionType.key);
}
onChangeConditionBlock = (event, blockNumber) => {
this.props.store.setConditionBlockNumber(blockNumber);
}
onChangeConditionDateTime = (event, datetime) => {
this.props.store.setConditionDateTime(datetime);
}
} }

View File

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

View File

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

View File

@ -21,6 +21,7 @@ import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/consta
import { ERRORS } from '~/util/validation'; import { ERRORS } from '~/util/validation';
import GasPriceEditor from './gasPriceEditor'; import GasPriceEditor from './gasPriceEditor';
import { CONDITIONS } from './store';
const { Store } = GasPriceEditor; const { Store } = GasPriceEditor;
@ -31,18 +32,30 @@ const HISTOGRAM = {
counts: [3, 4] counts: [3, 4]
}; };
const api = { let api;
eth: {
gasPrice: sinon.stub().resolves(GASPRICE)
},
parity: {
gasPriceHistogram: sinon.stub().resolves(HISTOGRAM)
}
};
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; let store = null;
beforeEach(() => {
createApi();
});
it('is available via GasPriceEditor.Store', () => { it('is available via GasPriceEditor.Store', () => {
expect(new Store(null, {})).to.be.ok; expect(new Store(null, {})).to.be.ok;
}); });
@ -65,6 +78,7 @@ describe('ui/GasPriceEditor/store', () => {
describe('constructor (defaults) when histogram not available', () => { describe('constructor (defaults) when histogram not available', () => {
const api = { const api = {
eth: { eth: {
blockNumber: sinon.stub().resolves(new BigNumber(2)),
gasPrice: sinon.stub().resolves(GASPRICE) gasPrice: sinon.stub().resolves(GASPRICE)
}, },
parity: { parity: {
@ -92,6 +106,67 @@ describe('ui/GasPriceEditor/store', () => {
store = new Store(null, { gasLimit: GASLIMIT }); 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', () => { describe('setEditing', () => {
it('sets the value', () => { it('sets the value', () => {
expect(store.isEditing).to.be.false; expect(store.isEditing).to.be.false;

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Button } from '~/ui';
import PlaygroundExample from '~/playground/playgroundExample'; import PlaygroundExample from '~/playground/playgroundExample';
import Modal from '../Modal'; import Modal from '../Modal';
@ -77,6 +78,29 @@ export default class PortalExample extends Component {
</Portal> </Portal>
</div> </div>
</PlaygroundExample> </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> </div>
); );
} }

View File

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

View File

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

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

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

View File

@ -14,35 +14,49 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { LinearProgress } from 'material-ui'; import { LinearProgress } from 'material-ui';
import { Step, Stepper, StepLabel } from 'material-ui/Stepper'; 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 { nodeOrStringProptype } from '~/util/proptypes';
import styles from '../modal.css'; import styles from './title.css';
export default class Title extends Component { export default class Title extends Component {
static propTypes = { static propTypes = {
activeStep: PropTypes.number,
busy: PropTypes.bool, busy: PropTypes.bool,
current: PropTypes.number, busySteps: PropTypes.array,
className: PropTypes.string,
steps: PropTypes.array, steps: PropTypes.array,
waiting: PropTypes.array,
title: nodeOrStringProptype() title: nodeOrStringProptype()
} }
render () { render () {
const { current, steps, title } = this.props; const { activeStep, className, steps, title } = this.props;
if (!title && !steps) {
return null;
}
return ( return (
<div className={ styles.title }> <div
<h3> className={
{ [styles.title, className].join(' ')
}
>
<ContainerTitle
title={
steps steps
? steps[current] ? steps[activeStep || 0]
: title : title
} }
</h3> />
{ this.renderSteps() } { this.renderSteps() }
{ this.renderWaiting() } { this.renderWaiting() }
</div> </div>
@ -50,7 +64,7 @@ export default class Title extends Component {
} }
renderSteps () { renderSteps () {
const { current, steps } = this.props; const { activeStep, steps } = this.props;
if (!steps) { if (!steps) {
return; return;
@ -58,7 +72,7 @@ export default class Title extends Component {
return ( return (
<div className={ styles.steps }> <div className={ styles.steps }>
<Stepper activeStep={ current }> <Stepper activeStep={ activeStep }>
{ this.renderTimeline() } { this.renderTimeline() }
</Stepper> </Stepper>
</div> </div>
@ -80,8 +94,8 @@ export default class Title extends Component {
} }
renderWaiting () { renderWaiting () {
const { current, busy, waiting } = this.props; const { activeStep, busy, busySteps } = this.props;
const isWaiting = busy || (waiting || []).includes(current); const isWaiting = busy || (busySteps || []).includes(activeStep);
if (!isWaiting) { if (!isWaiting) {
return null; return null;

View File

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

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

@ -0,0 +1,60 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import base32 from 'base32.js';
const BASE_URL = '.web.web3.site';
const ENCODER_OPTS = { type: 'crockford' };
export function encodePath (token, url) {
const encoder = new base32.Encoder(ENCODER_OPTS);
const chars = `${token}+${url}`
.split('')
.map((char) => char.charCodeAt(0));
return encoder
.write(chars) // add the characters to encode
.finalize(); // create the encoded string
}
export function encodeUrl (token, url) {
const encoded = encodePath(token, url)
.match(/.{1,63}/g) // split into 63-character chunks, max length is 64 for URLs parts
.join('.'); // add '.' between URL parts
return `${encoded}${BASE_URL}`;
}
// TODO: This export is really more a helper along the way of verifying the actual
// encoding (being able to decode test values from the node layer), than meant to
// be used as-is. Should the need arrise to decode URLs as well (instead of just
// producing), it would make sense to further split the output into the token/URL
export function decode (encoded) {
const decoder = new base32.Decoder(ENCODER_OPTS);
const sanitized = encoded
.replace(BASE_URL, '') // remove the BASE URL
.split('.') // split the string on the '.' (63-char boundaries)
.join(''); // combine without the '.'
return decoder
.write(sanitized) // add the string to decode
.finalize() // create the decoded buffer
.toString(); // create string from buffer
}
export {
BASE_URL
};

View File

@ -0,0 +1,83 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { BASE_URL, decode, encodePath, encodeUrl } from './dapplink';
const TEST_TOKEN = 'token';
const TEST_URL = 'https://parity.io';
const TEST_URL_LONG = 'http://some.very.very.very.long.long.long.domain.example.com';
const TEST_PREFIX = 'EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY';
const TEST_PREFIX_LONG = [
'EHQPPSBE5DM78X3G78QJYWVFDNJJWXK5E9WJWXK5E9WJWXK5E9WJWV3FDSKJWV3', 'FDSKJWV3FDSKJWS3FDNGPJVHECNW62VBGDHJJWRVFDM'
].join('.');
const TEST_RESULT = `${TEST_PREFIX}${BASE_URL}`;
const TEST_ENCODED = `${TEST_TOKEN}+${TEST_URL}`;
describe('util/ethlink', () => {
describe('decode', () => {
it('decodes into encoded url', () => {
expect(decode(TEST_PREFIX)).to.equal(TEST_ENCODED);
});
it('decodes full into encoded url', () => {
expect(decode(TEST_RESULT)).to.equal(TEST_ENCODED);
});
});
describe('encodePath', () => {
it('encodes a url/token combination', () => {
expect(encodePath(TEST_TOKEN, TEST_URL)).to.equal(TEST_PREFIX);
});
it('changes when token changes', () => {
expect(encodePath('test-token-2', TEST_URL)).not.to.equal(TEST_PREFIX);
});
it('changes when url changes', () => {
expect(encodePath(TEST_TOKEN, 'http://other.example.com')).not.to.equal(TEST_PREFIX);
});
});
describe('encodeUrl', () => {
it('encodes a url/token combination', () => {
expect(encodeUrl(TEST_TOKEN, TEST_URL)).to.equal(TEST_RESULT);
});
it('changes when token changes', () => {
expect(encodeUrl('test-token-2', TEST_URL)).not.to.equal(TEST_RESULT);
});
it('changes when url changes', () => {
expect(encodeUrl(TEST_TOKEN, 'http://other.example.com')).not.to.equal(TEST_RESULT);
});
describe('splitting', () => {
let encoded;
beforeEach(() => {
encoded = encodeUrl(TEST_TOKEN, TEST_URL_LONG);
});
it('splits long values into boundary parts', () => {
expect(encoded).to.equal(`${TEST_PREFIX_LONG}${BASE_URL}`);
});
it('first part 63 characters', () => {
expect(encoded.split('.')[0].length).to.equal(63);
});
});
});
});

View File

@ -0,0 +1,52 @@
/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.body {
background: #f80;
color: white;
opacity: 1;
max-width: 500px;
padding: 1em 4em 1em 2em;
position: fixed;
right: 1.5em;
top: 1.5em;
z-index: 1000;
.button {
background: rgba(0, 0, 0, 0.5);
color: white !important;
svg {
fill: white !important;
}
}
.buttonrow {
text-align: right;
}
p {
color: white;
}
.close {
cursor: pointer;
position: absolute;
right: 1em;
top: 1em;
}
}

View File

@ -0,0 +1,74 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component } from 'react';
import { FormattedMessage } from 'react-intl';
import { Button } from '~/ui';
import { CloseIcon, CheckIcon } from '~/ui/Icons';
import Store from './store';
import styles from './extension.css';
@observer
export default class Extension extends Component {
store = new Store();
render () {
const { showWarning } = this.store;
if (!showWarning) {
return null;
}
return (
<div className={ styles.body }>
<CloseIcon
className={ styles.close }
onClick={ this.onClose }
/>
<p>
<FormattedMessage
id='extension.intro'
defaultMessage='Parity now has an extension available for Chrome that allows safe browsing of Ethereum-enabled distributed applications. It is highly recommended that you install this extension to further enhance your Parity experience.'
/>
</p>
<p className={ styles.buttonrow }>
<Button
className={ styles.button }
icon={ <CheckIcon /> }
label={
<FormattedMessage
id='extension.install'
defaultMessage='Install the extension now'
/>
}
onClick={ this.onInstallClick }
/>
</p>
</div>
);
}
onClose = () => {
this.store.snoozeWarning();
}
onInstallClick = () => {
this.store.installExtension();
}
}

View File

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

View File

@ -0,0 +1,89 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
/* global chrome */
import { action, computed, observable } from 'mobx';
import store from 'store';
import browser from 'useragent.js/lib/browser';
const A_DAY = 24 * 60 * 60 * 1000;
const NEXT_DISPLAY = '_parity::extensionWarning::nextDisplay';
// 'https://chrome.google.com/webstore/detail/parity-ethereum-integrati/himekenlppkgeaoeddcliojfddemadig';
const EXTENSION_PAGE = 'https://chrome.google.com/webstore/detail/himekenlppkgeaoeddcliojfddemadig';
export default class Store {
@observable isInstalling = false;
@observable nextDisplay = 0;
@observable shouldInstall = false;
constructor () {
this.nextDisplay = store.get(NEXT_DISPLAY) || 0;
this.testInstall();
}
@computed get showWarning () {
return !this.isInstalling && this.shouldInstall && (Date.now() > this.nextDisplay);
}
@action setInstalling = (isInstalling) => {
this.isInstalling = isInstalling;
}
@action snoozeWarning = (sleep = A_DAY) => {
this.nextDisplay = Date.now() + sleep;
store.set(NEXT_DISPLAY, this.nextDisplay);
}
@action testInstall = () => {
this.shouldInstall = this.readStatus();
}
readStatus = () => {
const hasExtension = Symbol.for('parity.extension') in window;
const ua = browser.analyze(navigator.userAgent || '');
if (hasExtension) {
return false;
}
return (ua || {}).name.toLowerCase() === 'chrome';
}
installExtension = () => {
this.setInstalling(true);
return new Promise((resolve, reject) => {
const link = document.createElement('link');
link.setAttribute('rel', 'chrome-webstore-item');
link.setAttribute('href', EXTENSION_PAGE);
document.querySelector('head').appendChild(link);
if (chrome && chrome.webstore && chrome.webstore.install) {
chrome.webstore.install(EXTENSION_PAGE, resolve, reject);
} else {
reject(new Error('Direct installation failed.'));
}
})
.catch((error) => {
console.warn('Unable to perform direct install', error);
window.open(EXTENSION_PAGE, '_blank');
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,48 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import AddressBar from './';
let component;
let store;
function createStore () {
store = {
nextUrl: 'https://parity.io'
};
return store;
}
function render (props = {}) {
component = shallow(
<AddressBar
className='testClass'
store={ createStore() }
/>
);
return component;
}
describe('views/Web/AddressBar', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
});

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

@ -0,0 +1,158 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { action, computed, observable, transaction } from 'mobx';
import localStore from 'store';
import { parse as parseUrl } from 'url';
import { encodePath, encodeUrl } from '~/util/dapplink';
const DEFAULT_URL = 'https://mkr.market';
const LS_LAST_ADDRESS = '_parity::webLastAddress';
const hasProtocol = /^https?:\/\//;
let instance = null;
export default class Store {
@observable counter = Date.now();
@observable currentUrl = null;
@observable history = [];
@observable isLoading = false;
@observable parsedUrl = null;
@observable nextUrl = null;
@observable token = null;
constructor (api) {
this._api = api;
this.nextUrl = this.currentUrl = this.loadLastUrl();
}
@computed get encodedPath () {
return `${this._api.dappsUrl}/web/${encodePath(this.token, this.currentUrl)}?t=${this.counter}`;
}
@computed get encodedUrl () {
return `http://${encodeUrl(this.token, this.currentUrl)}:${this._api.dappsPort}?t=${this.counter}`;
}
@computed get frameId () {
return `_web_iframe_${this.counter}`;
}
@computed get isPristine () {
return this.currentUrl === this.nextUrl;
}
@action gotoUrl = (_url) => {
transaction(() => {
let url = (_url || this.nextUrl).trim().replace(/\/+$/, '');
if (!hasProtocol.test(url)) {
url = `https://${url}`;
}
this.setNextUrl(url);
this.setCurrentUrl(this.nextUrl);
});
}
@action reload = () => {
transaction(() => {
this.setLoading(true);
this.counter = Date.now();
});
}
@action restoreUrl = () => {
this.setNextUrl(this.currentUrl);
}
@action setHistory = (history) => {
this.history = history;
}
@action setLoading = (isLoading) => {
this.isLoading = isLoading;
}
@action setToken = (token) => {
this.token = token;
}
@action setCurrentUrl = (_url) => {
const url = _url || this.currentUrl;
transaction(() => {
this.currentUrl = url;
this.parsedUrl = parseUrl(url);
this.saveLastUrl();
this.reload();
});
}
@action setNextUrl = (url) => {
this.nextUrl = url;
}
generateToken = () => {
this.setToken(null);
return this._api.signer
.generateWebProxyAccessToken()
.then((token) => {
this.setToken(token);
})
.catch((error) => {
console.warn('generateToken', error);
});
}
loadHistory = () => {
return this._api.parity
.listRecentDapps()
.then((apps) => {
this.setHistory(apps);
})
.catch((error) => {
console.warn('loadHistory', error);
});
}
loadLastUrl = () => {
return localStore.get(LS_LAST_ADDRESS) || DEFAULT_URL;
}
saveLastUrl = () => {
return localStore.set(LS_LAST_ADDRESS, this.currentUrl);
}
static get (api) {
if (!instance) {
instance = new Store(api);
}
return instance;
}
}
export {
DEFAULT_URL,
LS_LAST_ADDRESS
};

View File

@ -0,0 +1,202 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
import Store from './store';
const TEST_HISTORY = ['somethingA', 'somethingB'];
const TEST_TOKEN = 'testing-123';
const TEST_URL1 = 'http://some.test.domain.com';
const TEST_URL2 = 'http://something.different.com';
let api;
let store;
function createApi () {
api = {
dappsPort: 8080,
dappsUrl: 'http://home.web3.site:8080',
parity: {
listRecentDapps: sinon.stub().resolves(TEST_HISTORY)
},
signer: {
generateWebProxyAccessToken: sinon.stub().resolves(TEST_TOKEN)
}
};
return api;
}
function create () {
store = new Store(createApi());
return store;
}
describe('views/Web/Store', () => {
beforeEach(() => {
create();
});
describe('@action', () => {
describe('gotoUrl', () => {
it('uses the nextUrl when none specified', () => {
store.setNextUrl('https://parity.io');
store.gotoUrl();
expect(store.currentUrl).to.equal('https://parity.io');
});
it('adds https when no protocol', () => {
store.gotoUrl('google.com');
expect(store.currentUrl).to.equal('https://google.com');
});
});
describe('restoreUrl', () => {
it('sets the nextUrl to the currentUrl', () => {
store.setCurrentUrl(TEST_URL1);
store.setNextUrl(TEST_URL2);
store.restoreUrl();
expect(store.nextUrl).to.equal(TEST_URL1);
});
});
describe('setCurrentUrl', () => {
beforeEach(() => {
store.setCurrentUrl(TEST_URL1);
});
it('sets the url', () => {
expect(store.currentUrl).to.equal(TEST_URL1);
});
});
describe('setHistory', () => {
it('sets the history', () => {
store.setHistory(TEST_HISTORY);
expect(store.history.peek()).to.deep.equal(TEST_HISTORY);
});
});
describe('setLoading', () => {
beforeEach(() => {
store.setLoading(true);
});
it('sets the loading state (true)', () => {
expect(store.isLoading).to.be.true;
});
it('sets the loading state (false)', () => {
store.setLoading(false);
expect(store.isLoading).to.be.false;
});
});
describe('setNextUrl', () => {
it('sets the url', () => {
store.setNextUrl(TEST_URL1);
expect(store.nextUrl).to.equal(TEST_URL1);
});
});
describe('setToken', () => {
it('sets the token', () => {
store.setToken(TEST_TOKEN);
expect(store.token).to.equal(TEST_TOKEN);
});
});
});
describe('@computed', () => {
describe('encodedUrl', () => {
describe('encodedPath', () => {
it('encodes current', () => {
store.setCurrentUrl(TEST_URL1);
expect(store.encodedPath).to.match(
/http:\/\/home\.web3\.site:8080\/web\/DSTPRV1BD1T78W1T5WQQ6VVDCMQ78SBKEGQ68VVDC5MPWBK3DXPG\?t=[0-9]*$/
);
});
});
it('encodes current', () => {
store.setCurrentUrl(TEST_URL1);
expect(store.encodedUrl).to.match(
/^http:\/\/DSTPRV1BD1T78W1T5WQQ6VVDCMQ78SBKEGQ68VVDC5MPWBK3DXPG\.web\.web3\.site:8080\?t=[0-9]*$/
);
});
});
describe('frameId', () => {
it('creates an id', () => {
expect(store.frameId).to.be.ok;
});
});
describe('isPristine', () => {
it('is true when current === next', () => {
store.setCurrentUrl(TEST_URL1);
store.setNextUrl(TEST_URL1);
expect(store.isPristine).to.be.true;
});
it('is false when current !== next', () => {
store.setCurrentUrl(TEST_URL1);
store.setNextUrl(TEST_URL2);
expect(store.isPristine).to.be.false;
});
});
});
describe('operations', () => {
describe('generateToken', () => {
beforeEach(() => {
return store.generateToken();
});
it('calls signer_generateWebProxyAccessToken', () => {
expect(api.signer.generateWebProxyAccessToken).to.have.been.calledOnce;
});
it('sets the token as retrieved', () => {
expect(store.token).to.equal(TEST_TOKEN);
});
});
describe('loadHistory', () => {
beforeEach(() => {
return store.loadHistory();
});
it('calls parity_listRecentDapps', () => {
expect(api.parity.listRecentDapps).to.have.been.calledOnce;
});
it('sets the history as retrieved', () => {
expect(store.history.peek()).to.deep.equal(TEST_HISTORY);
});
});
});
});

View File

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

View File

@ -0,0 +1,56 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import Web from './';
const TEST_URL = 'https://mkr.market';
let api;
let component;
function createApi () {
api = {};
return api;
}
function render (url = TEST_URL) {
component = shallow(
<Web params={ { url } } />,
{
context: { api: createApi() }
}
);
return component;
}
describe('views/Web', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('renders loading with no token', () => {
expect(component.find('FormattedMessage').props().id).to.equal('web.requestToken');
});
});

View File

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

View File

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

View File

@ -30,7 +30,8 @@ use ethcore::service::ClientIoMessage;
use ethcore::snapshot::service::Service as SnapshotService; use ethcore::snapshot::service::Service as SnapshotService;
use ethcore::snapshot::{RestorationStatus, SnapshotService as SS}; use ethcore::snapshot::{RestorationStatus, SnapshotService as SS};
use number_prefix::{binary_prefix, Standalone, Prefixed}; 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; use rlp::View;
pub struct Informant { pub struct Informant {
@ -41,6 +42,7 @@ pub struct Informant {
snapshot: Option<Arc<SnapshotService>>, snapshot: Option<Arc<SnapshotService>>,
sync: Option<Arc<SyncProvider>>, sync: Option<Arc<SyncProvider>>,
net: Option<Arc<ManageNetwork>>, net: Option<Arc<ManageNetwork>>,
rpc_stats: Option<Arc<RpcStats>>,
last_import: Mutex<Instant>, last_import: Mutex<Instant>,
skipped: AtomicUsize, skipped: AtomicUsize,
skipped_txs: AtomicUsize, skipped_txs: AtomicUsize,
@ -63,13 +65,20 @@ pub trait MillisecondDuration {
impl MillisecondDuration for Duration { impl MillisecondDuration for Duration {
fn as_milliseconds(&self) -> u64 { 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 { impl Informant {
/// Make a new instance potentially `with_color` output. /// 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 { Informant {
report: RwLock::new(None), report: RwLock::new(None),
last_tick: RwLock::new(Instant::now()), last_tick: RwLock::new(Instant::now()),
@ -78,6 +87,7 @@ impl Informant {
snapshot: snapshot, snapshot: snapshot,
sync: sync, sync: sync,
net: net, net: net,
rpc_stats: rpc_stats,
last_import: Mutex::new(Instant::now()), last_import: Mutex::new(Instant::now()),
skipped: AtomicUsize::new(0), skipped: AtomicUsize::new(0),
skipped_txs: AtomicUsize::new(0), skipped_txs: AtomicUsize::new(0),
@ -102,6 +112,7 @@ impl Informant {
let cache_info = self.client.blockchain_cache_info(); let cache_info = self.client.blockchain_cache_info();
let network_config = self.net.as_ref().map(|n| n.network_config()); let network_config = self.net.as_ref().map(|n| n.network_config());
let sync_status = self.sync.as_ref().map(|s| s.status()); 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 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| 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, false => t,
}; };
info!(target: "import", "{} {} {}", info!(target: "import", "{} {} {} {}",
match importing { match importing {
true => match snapshot_sync { 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!("{:>8}", format!("#{}", chain_info.best_block_number))),
paint(White.bold(), format!("{}", chain_info.best_block_hash)), 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)))), Some(ref sync_info) => format!(" {} sync", paint(Blue.bold(), format!("{:>8}", format_bytes(sync_info.mem_used)))),
_ => String::new(), _ => 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); *write_report = Some(report);

View File

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

View File

@ -14,22 +14,25 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::cmp::PartialEq;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::collections::HashSet; use std::collections::HashSet;
use std::cmp::PartialEq;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; 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; 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 hash_fetch::fetch::Client as FetchClient;
use jsonrpc_core::{MetaIoHandler};
use updater::Updater;
use util::RotatingLogger;
#[derive(Debug, PartialEq, Clone, Eq, Hash)] #[derive(Debug, PartialEq, Clone, Eq, Hash)]
pub enum Api { 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::*; 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 &[] // it's turned into vector, cause ont of the cases requires &[]
let apis = apis.list_apis().into_iter().collect::<Vec<_>>(); let apis = apis.list_apis().into_iter().collect::<Vec<_>>();
for api in &apis { 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); add_signing_methods!(ParitySigning, handler, deps);
}, },
Api::ParityAccounts => { 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 => { Api::ParitySet => {
handler.extend_with(ParitySetClient::new( handler.extend_with(ParitySetClient::new(

View File

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

View File

@ -15,18 +15,21 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::io; use std::io;
use std::sync::Arc;
use std::path::PathBuf; use std::path::PathBuf;
use ansi_term::Colour; use std::sync::Arc;
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};
pub use ethcore_signer::Server as SignerServer; 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"; const CODES_FILENAME: &'static str = "authcodes";
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
@ -55,6 +58,7 @@ pub struct Dependencies {
pub panic_handler: Arc<PanicHandler>, pub panic_handler: Arc<PanicHandler>,
pub apis: Arc<rpc_apis::Dependencies>, pub apis: Arc<rpc_apis::Dependencies>,
pub remote: Remote, pub remote: Remote,
pub rpc_stats: Arc<RpcStats>,
} }
pub struct NewToken { 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."); info!("If you do not intend this, exit now.");
} }
let server = server.skip_origin_validation(conf.skip_origin_validation); 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); let handler = RpcHandler::new(Arc::new(apis), deps.remote);
server.start(addr, handler) server.start(addr, handler)
}; };

View File

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

View File

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

View File

@ -17,7 +17,7 @@
use std::fmt::Debug; use std::fmt::Debug;
use std::ops::Deref; use std::ops::Deref;
use rlp; use rlp;
use util::{Address, H256, U256, Uint, Bytes}; use util::{Address, H520, H256, U256, Uint, Bytes};
use util::bytes::ToPretty; use util::bytes::ToPretty;
use util::sha3::Hashable; use util::sha3::Hashable;
@ -112,6 +112,14 @@ pub fn execute<C, M>(client: &C, miner: &M, accounts: &AccountProvider, payload:
ConfirmationPayload::Signature(address, data) => { ConfirmationPayload::Signature(address, data) => {
signature(accounts, address, data.sha3(), pass) signature(accounts, address, data.sha3(), pass)
.map(|result| result .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(RpcH520::from)
.map(ConfirmationResponse::Signature) .map(ConfirmationResponse::Signature)
) )

View File

@ -0,0 +1,301 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! RPC Requests Statistics
use std::fmt;
use std::sync::Arc;
use std::sync::atomic::{self, AtomicUsize};
use std::time;
use futures::Future;
use jsonrpc_core as rpc;
use order_stat;
use util::RwLock;
const RATE_SECONDS: usize = 10;
const STATS_SAMPLES: usize = 60;
struct RateCalculator {
era: time::Instant,
samples: [u16; RATE_SECONDS],
}
impl fmt::Debug for RateCalculator {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "{} req/s", self.rate())
}
}
impl Default for RateCalculator {
fn default() -> Self {
RateCalculator {
era: time::Instant::now(),
samples: [0; RATE_SECONDS],
}
}
}
impl RateCalculator {
fn elapsed(&self) -> u64 {
self.era.elapsed().as_secs()
}
pub fn tick(&mut self) -> u16 {
if self.elapsed() >= RATE_SECONDS as u64 {
self.era = time::Instant::now();
self.samples[0] = 0;
}
let pos = self.elapsed() as usize % RATE_SECONDS;
let next = (pos + 1) % RATE_SECONDS;
self.samples[next] = 0;
self.samples[pos] = self.samples[pos].saturating_add(1);
self.samples[pos]
}
fn current_rate(&self) -> usize {
let now = match self.elapsed() {
i if i >= RATE_SECONDS as u64 => RATE_SECONDS,
i => i as usize + 1,
};
let sum: usize = self.samples[0..now].iter().map(|x| *x as usize).sum();
sum / now
}
pub fn rate(&self) -> usize {
if self.elapsed() > RATE_SECONDS as u64 {
0
} else {
self.current_rate()
}
}
}
struct StatsCalculator<T = u32> {
filled: bool,
idx: usize,
samples: [T; STATS_SAMPLES],
}
impl<T: Default + Copy> Default for StatsCalculator<T> {
fn default() -> Self {
StatsCalculator {
filled: false,
idx: 0,
samples: [T::default(); STATS_SAMPLES],
}
}
}
impl<T: fmt::Display + Default + Copy + Ord> fmt::Debug for StatsCalculator<T> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "median: {} ms", self.approximated_median())
}
}
impl<T: Default + Copy + Ord> StatsCalculator<T> {
pub fn add(&mut self, sample: T) {
self.idx += 1;
if self.idx >= STATS_SAMPLES {
self.filled = true;
self.idx = 0;
}
self.samples[self.idx] = sample;
}
/// Returns aproximate of media
pub fn approximated_median(&self) -> T {
let mut copy = [T::default(); STATS_SAMPLES];
copy.copy_from_slice(&self.samples);
let bound = if self.filled { STATS_SAMPLES } else { self.idx + 1 };
let (_, &mut median) = order_stat::median_of_medians(&mut copy[0..bound]);
median
}
}
/// RPC Statistics
#[derive(Default, Debug)]
pub struct RpcStats {
requests: RwLock<RateCalculator>,
roundtrips: RwLock<StatsCalculator<u32>>,
active_sessions: AtomicUsize,
}
impl RpcStats {
/// Count session opened
pub fn open_session(&self) {
self.active_sessions.fetch_add(1, atomic::Ordering::SeqCst);
}
/// Count session closed.
/// Silently overflows if closing unopened session.
pub fn close_session(&self) {
self.active_sessions.fetch_sub(1, atomic::Ordering::SeqCst);
}
/// Count request. Returns number of requests in current second.
pub fn count_request(&self) -> u16 {
self.requests.write().tick()
}
/// Add roundtrip time (microseconds)
pub fn add_roundtrip(&self, microseconds: u32) {
self.roundtrips.write().add(microseconds)
}
/// Returns number of open sessions
pub fn sessions(&self) -> usize {
self.active_sessions.load(atomic::Ordering::Relaxed)
}
/// Returns requests rate
pub fn requests_rate(&self) -> usize {
self.requests.read().rate()
}
/// Returns approximated roundtrip in microseconds
pub fn approximated_roundtrip(&self) -> u32 {
self.roundtrips.read().approximated_median()
}
}
/// Notifies about RPC activity.
pub trait ActivityNotifier: Send + Sync + 'static {
/// Activity on RPC interface
fn active(&self);
}
/// Stats-counting RPC middleware
pub struct Middleware<T: ActivityNotifier = ClientNotifier> {
stats: Arc<RpcStats>,
notifier: T,
}
impl<T: ActivityNotifier> Middleware<T> {
/// Create new Middleware with stats counter and activity notifier.
pub fn new(stats: Arc<RpcStats>, notifier: T) -> Self {
Middleware {
stats: stats,
notifier: notifier,
}
}
fn as_micro(dur: time::Duration) -> u32 {
(dur.as_secs() * 1_000_000) as u32 + dur.subsec_nanos() / 1_000
}
}
impl<M: rpc::Metadata, T: ActivityNotifier> rpc::Middleware<M> for Middleware<T> {
fn on_request<F>(&self, request: rpc::Request, meta: M, process: F) -> rpc::FutureResponse where
F: FnOnce(rpc::Request, M) -> rpc::FutureResponse,
{
let start = time::Instant::now();
let response = process(request, meta);
self.notifier.active();
let stats = self.stats.clone();
stats.count_request();
response.map(move |res| {
stats.add_roundtrip(Self::as_micro(start.elapsed()));
res
}).boxed()
}
}
/// Client Notifier
pub struct ClientNotifier {
/// Client
pub client: Arc<::ethcore::client::Client>,
}
impl ActivityNotifier for ClientNotifier {
fn active(&self) {
self.client.keep_alive()
}
}
#[cfg(test)]
mod tests {
use super::{RateCalculator, StatsCalculator, RpcStats};
#[test]
fn should_calculate_rate() {
// given
let mut avg = RateCalculator::default();
// when
avg.tick();
avg.tick();
avg.tick();
let rate = avg.rate();
// then
assert_eq!(rate, 3usize);
}
#[test]
fn should_approximate_median() {
// given
let mut stats = StatsCalculator::default();
stats.add(5);
stats.add(100);
stats.add(3);
stats.add(15);
stats.add(20);
stats.add(6);
// when
let median = stats.approximated_median();
// then
assert_eq!(median, 5);
}
#[test]
fn should_count_rpc_stats() {
// given
let stats = RpcStats::default();
assert_eq!(stats.sessions(), 0);
assert_eq!(stats.requests_rate(), 0);
assert_eq!(stats.approximated_roundtrip(), 0);
// when
stats.open_session();
stats.close_session();
stats.open_session();
stats.count_request();
stats.count_request();
stats.add_roundtrip(125);
// then
assert_eq!(stats.sessions(), 1);
assert_eq!(stats.requests_rate(), 2);
assert_eq!(stats.approximated_roundtrip(), 125);
}
#[test]
fn should_be_sync_and_send() {
let stats = RpcStats::default();
is_sync(stats);
}
fn is_sync<F: Send + Sync>(x: F) {
drop(x)
}
}

View File

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

View File

@ -258,20 +258,6 @@ fn check_known<C>(client: &C, number: BlockNumber) -> Result<(), Error> where C:
const MAX_QUEUE_SIZE_TO_MINE_ON: usize = 4; // because uncles go back 6. 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)] #[cfg(windows)]
static SOLC: &'static str = "solc.exe"; 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; type Metadata = Metadata;
fn protocol_version(&self) -> Result<String, Error> { fn protocol_version(&self) -> Result<String, Error> {
self.active()?;
let version = take_weak!(self.sync).status().protocol_version.to_owned(); let version = take_weak!(self.sync).status().protocol_version.to_owned();
Ok(format!("{}", version)) 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> { fn syncing(&self) -> Result<SyncStatus, Error> {
use ethcore::snapshot::RestorationStatus; use ethcore::snapshot::RestorationStatus;
self.active()?;
let status = take_weak!(self.sync).status(); let status = take_weak!(self.sync).status();
let client = take_weak!(self.client); let client = take_weak!(self.client);
let snapshot_status = take_weak!(self.snapshot).status(); 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 dapp = meta.dapp_id.unwrap_or_default();
let author = move || { let author = move || {
self.active()?;
let mut miner = take_weak!(self.miner).author(); let mut miner = take_weak!(self.miner).author();
if miner == 0.into() { if miner == 0.into() {
let accounts = self.dapp_accounts(dapp.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> { fn is_mining(&self) -> Result<bool, Error> {
self.active()?;
Ok(take_weak!(self.miner).is_sealing()) Ok(take_weak!(self.miner).is_sealing())
} }
fn hashrate(&self) -> Result<RpcU256, Error> { fn hashrate(&self) -> Result<RpcU256, Error> {
self.active()?;
Ok(RpcU256::from(self.external_miner.hashrate())) Ok(RpcU256::from(self.external_miner.hashrate()))
} }
fn gas_price(&self) -> Result<RpcU256, Error> { fn gas_price(&self) -> Result<RpcU256, Error> {
self.active()?;
let (client, miner) = (take_weak!(self.client), take_weak!(self.miner)); let (client, miner) = (take_weak!(self.client), take_weak!(self.miner));
Ok(RpcU256::from(default_gas_price(&*client, &*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 dapp = meta.dapp_id.unwrap_or_default();
let accounts = move || { let accounts = move || {
self.active()?;
let accounts = self.dapp_accounts(dapp.into())?; let accounts = self.dapp_accounts(dapp.into())?;
Ok(accounts.into_iter().map(Into::into).collect()) 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> { fn block_number(&self) -> Result<RpcU256, Error> {
self.active()?;
Ok(RpcU256::from(take_weak!(self.client).chain_info().best_block_number)) 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 address = address.into();
let inner = || { let inner = || {
self.active()?;
match num.0.clone() { match num.0.clone() {
BlockNumber::Pending => Ok(take_weak!(self.miner).balance(&*take_weak!(self.client), &address).into()), BlockNumber::Pending => Ok(take_weak!(self.miner).balance(&*take_weak!(self.client), &address).into()),
id => { 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 position: U256 = RpcU256::into(pos);
let inner = || { let inner = || {
self.active()?;
match num.0.clone() { match num.0.clone() {
BlockNumber::Pending => Ok(take_weak!(self.miner).storage_at(&*take_weak!(self.client), &address, &H256::from(position)).into()), BlockNumber::Pending => Ok(take_weak!(self.miner).storage_at(&*take_weak!(self.client), &address, &H256::from(position)).into()),
id => { 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> { fn transaction_count(&self, address: RpcH160, num: Trailing<BlockNumber>) -> BoxFuture<RpcU256, Error> {
let address: Address = RpcH160::into(address); let address: Address = RpcH160::into(address);
let inner = move || { let inner = move || {
self.active()?;
match num.0.clone() { match num.0.clone() {
BlockNumber::Pending => Ok(take_weak!(self.miner).nonce(&*take_weak!(self.client), &address).into()), BlockNumber::Pending => Ok(take_weak!(self.miner).nonce(&*take_weak!(self.client), &address).into()),
id => { 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> { fn block_transaction_count_by_hash(&self, hash: RpcH256) -> BoxFuture<Option<RpcU256>, Error> {
let inner = || { let inner = || {
self.active()?;
Ok(take_weak!(self.client).block(BlockId::Hash(hash.into())) Ok(take_weak!(self.client).block(BlockId::Hash(hash.into()))
.map(|block| block.transactions_count().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> { fn block_transaction_count_by_number(&self, num: BlockNumber) -> BoxFuture<Option<RpcU256>, Error> {
let inner = || { let inner = || {
self.active()?;
match num { match num {
BlockNumber::Pending => Ok(Some( BlockNumber::Pending => Ok(Some(
take_weak!(self.miner).status().transactions_in_pending_block.into() 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> { fn block_uncles_count_by_hash(&self, hash: RpcH256) -> BoxFuture<Option<RpcU256>, Error> {
let inner = || { let inner = || {
self.active()?;
Ok(take_weak!(self.client).block(BlockId::Hash(hash.into())) Ok(take_weak!(self.client).block(BlockId::Hash(hash.into()))
.map(|block| block.uncles_count().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> { fn block_uncles_count_by_number(&self, num: BlockNumber) -> BoxFuture<Option<RpcU256>, Error> {
let inner = || { let inner = || {
self.active()?;
match num { match num {
BlockNumber::Pending => Ok(Some(0.into())), BlockNumber::Pending => Ok(Some(0.into())),
_ => Ok( _ => 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 address: Address = RpcH160::into(address);
let inner = || { let inner = || {
self.active()?;
match num.0.clone() { match num.0.clone() {
BlockNumber::Pending => Ok(take_weak!(self.miner).code(&*take_weak!(self.client), &address).map_or_else(Bytes::default, Bytes::new)), BlockNumber::Pending => Ok(take_weak!(self.miner).code(&*take_weak!(self.client), &address).map_or_else(Bytes::default, Bytes::new)),
id => { 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> { fn block_by_hash(&self, hash: RpcH256, include_txs: bool) -> BoxFuture<Option<RichBlock>, Error> {
let inner = || { future::done(self.block(BlockId::Hash(hash.into()), include_txs)).boxed()
self.active()?;
self.block(BlockId::Hash(hash.into()), include_txs)
};
future::done(inner()).boxed()
} }
fn block_by_number(&self, num: BlockNumber, include_txs: bool) -> BoxFuture<Option<RichBlock>, Error> { fn block_by_number(&self, num: BlockNumber, include_txs: bool) -> BoxFuture<Option<RichBlock>, Error> {
let inner = || { future::done(self.block(num.into(), include_txs)).boxed()
self.active()?;
self.block(num.into(), include_txs)
};
future::done(inner()).boxed()
} }
fn transaction_by_hash(&self, hash: RpcH256) -> Result<Option<Transaction>, Error> { fn transaction_by_hash(&self, hash: RpcH256) -> Result<Option<Transaction>, Error> {
self.active()?;
let hash: H256 = hash.into(); let hash: H256 = hash.into();
let miner = take_weak!(self.miner); let miner = take_weak!(self.miner);
let client = take_weak!(self.client); 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> { 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())) 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> { 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())) self.transaction(TransactionId::Location(num.into(), index.value()))
} }
fn transaction_receipt(&self, hash: RpcH256) -> Result<Option<Receipt>, Error> { fn transaction_receipt(&self, hash: RpcH256) -> Result<Option<Receipt>, Error> {
self.active()?;
let miner = take_weak!(self.miner); let miner = take_weak!(self.miner);
let best_block = take_weak!(self.client).chain_info().best_block_number; let best_block = take_weak!(self.client).chain_info().best_block_number;
let hash: H256 = hash.into(); 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> { 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() }) 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> { 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() }) self.uncle(UncleId { block: num.into(), position: index.value() })
} }
fn compilers(&self) -> Result<Vec<String>, Error> { fn compilers(&self) -> Result<Vec<String>, Error> {
self.active()?;
let mut compilers = vec![]; let mut compilers = vec![];
if Command::new(SOLC).output().is_ok() { if Command::new(SOLC).output().is_ok() {
compilers.push("solidity".to_owned()) 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> { 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 no_new_work_timeout = no_new_work_timeout.0;
let client = take_weak!(self.client); 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> { fn submit_work(&self, nonce: RpcH64, pow_hash: RpcH256, mix_hash: RpcH256) -> Result<bool, Error> {
self.active()?;
let nonce: H64 = nonce.into(); let nonce: H64 = nonce.into();
let pow_hash: H256 = pow_hash.into(); let pow_hash: H256 = pow_hash.into();
let mix_hash: H256 = mix_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> { fn submit_hashrate(&self, rate: RpcU256, id: RpcH256) -> Result<bool, Error> {
self.active()?;
self.external_miner.submit_hashrate(rate.into(), id.into()); self.external_miner.submit_hashrate(rate.into(), id.into());
Ok(true) Ok(true)
} }
fn send_raw_transaction(&self, raw: Bytes) -> Result<RpcH256, Error> { fn send_raw_transaction(&self, raw: Bytes) -> Result<RpcH256, Error> {
self.active()?;
UntrustedRlp::new(&raw.into_vec()).as_val() UntrustedRlp::new(&raw.into_vec()).as_val()
.map_err(errors::from_rlp_error) .map_err(errors::from_rlp_error)
.and_then(|tx| SignedTransaction::new(tx).map_err(errors::from_transaction_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> { fn call(&self, request: CallRequest, num: Trailing<BlockNumber>) -> Result<Bytes, Error> {
self.active()?;
let request = CallRequest::into(request); let request = CallRequest::into(request);
let signed = self.sign_call(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> { fn estimate_gas(&self, request: CallRequest, num: Trailing<BlockNumber>) -> Result<RpcU256, Error> {
self.active()?;
let request = CallRequest::into(request); let request = CallRequest::into(request);
let signed = self.sign_call(request)?; let signed = self.sign_call(request)?;
take_weak!(self.client).estimate_gas(&signed, num.0.into()) 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> { fn compile_lll(&self, _: String) -> Result<Bytes, Error> {
self.active()?;
rpc_unimplemented!() rpc_unimplemented!()
} }
fn compile_serpent(&self, _: String) -> Result<Bytes, Error> { fn compile_serpent(&self, _: String) -> Result<Bytes, Error> {
self.active()?;
rpc_unimplemented!() rpc_unimplemented!()
} }
fn compile_solidity(&self, code: String) -> Result<Bytes, Error> { fn compile_solidity(&self, code: String) -> Result<Bytes, Error> {
self.active()?;
let maybe_child = Command::new(SOLC) let maybe_child = Command::new(SOLC)
.arg("--bin") .arg("--bin")
.arg("--optimize") .arg("--optimize")

View File

@ -50,19 +50,12 @@ impl<C, M> EthFilterClient<C, M> where
polls: Mutex::new(PollManager::new()), 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> impl<C, M> EthFilter for EthFilterClient<C, M>
where C: BlockChainClient + 'static, M: MinerService + 'static where C: BlockChainClient + 'static, M: MinerService + 'static
{ {
fn new_filter(&self, filter: Filter) -> Result<RpcU256, Error> { fn new_filter(&self, filter: Filter) -> Result<RpcU256, Error> {
self.active()?;
let mut polls = self.polls.lock(); let mut polls = self.polls.lock();
let block_number = take_weak!(self.client).chain_info().best_block_number; let block_number = take_weak!(self.client).chain_info().best_block_number;
let id = polls.create_poll(PollFilter::Logs(block_number, Default::default(), filter)); 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> { fn new_block_filter(&self) -> Result<RpcU256, Error> {
self.active()?;
let mut polls = self.polls.lock(); let mut polls = self.polls.lock();
let id = polls.create_poll(PollFilter::Block(take_weak!(self.client).chain_info().best_block_number)); let id = polls.create_poll(PollFilter::Block(take_weak!(self.client).chain_info().best_block_number));
Ok(id.into()) Ok(id.into())
} }
fn new_pending_transaction_filter(&self) -> Result<RpcU256, Error> { fn new_pending_transaction_filter(&self) -> Result<RpcU256, Error> {
self.active()?;
let mut polls = self.polls.lock(); let mut polls = self.polls.lock();
let best_block = take_weak!(self.client).chain_info().best_block_number; let best_block = take_weak!(self.client).chain_info().best_block_number;
let pending_transactions = take_weak!(self.miner).pending_transactions_hashes(best_block); 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> { fn filter_changes(&self, index: Index) -> Result<FilterChanges, Error> {
self.active()?;
let client = take_weak!(self.client); let client = take_weak!(self.client);
let mut polls = self.polls.lock(); let mut polls = self.polls.lock();
match polls.poll_mut(&index.value()) { 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> { fn filter_logs(&self, index: Index) -> Result<Vec<Log>, Error> {
self.active()?;
let mut polls = self.polls.lock(); let mut polls = self.polls.lock();
match polls.poll(&index.value()) { match polls.poll(&index.value()) {
Some(&PollFilter::Logs(ref _block_number, ref _previous_log, ref filter)) => { 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> { fn uninstall_filter(&self, index: Index) -> Result<bool, Error> {
self.active()?;
self.polls.lock().remove_poll(&index.value()); self.polls.lock().remove_poll(&index.value());
Ok(true) Ok(true)
} }

View File

@ -101,12 +101,6 @@ impl<C, M, S: ?Sized, U> ParityClient<C, M, S, U> where
dapps_port: dapps_port, 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 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; type Metadata = Metadata;
fn accounts_info(&self, dapp: Trailing<DappId>) -> Result<BTreeMap<String, BTreeMap<String, String>>, Error> { fn accounts_info(&self, dapp: Trailing<DappId>) -> Result<BTreeMap<String, BTreeMap<String, String>>, Error> {
self.active()?;
let dapp = dapp.0; let dapp = dapp.0;
let store = take_weak!(self.accounts); 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> { fn default_account(&self, meta: Self::Metadata) -> BoxFuture<H160, Error> {
let dapp_id = meta.dapp_id.unwrap_or_default(); let dapp_id = meta.dapp_id.unwrap_or_default();
let default_account = move || { let default_account = move || {
self.active()?;
Ok(take_weak!(self.accounts) Ok(take_weak!(self.accounts)
.dapps_addresses(dapp_id.into()) .dapps_addresses(dapp_id.into())
.ok() .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> { fn transactions_limit(&self) -> Result<usize, Error> {
self.active()?;
Ok(take_weak!(self.miner).transactions_limit()) Ok(take_weak!(self.miner).transactions_limit())
} }
fn min_gas_price(&self) -> Result<U256, Error> { fn min_gas_price(&self) -> Result<U256, Error> {
self.active()?;
Ok(U256::from(take_weak!(self.miner).minimal_gas_price())) Ok(U256::from(take_weak!(self.miner).minimal_gas_price()))
} }
fn extra_data(&self) -> Result<Bytes, Error> { fn extra_data(&self) -> Result<Bytes, Error> {
self.active()?;
Ok(Bytes::new(take_weak!(self.miner).extra_data())) Ok(Bytes::new(take_weak!(self.miner).extra_data()))
} }
fn gas_floor_target(&self) -> Result<U256, Error> { fn gas_floor_target(&self) -> Result<U256, Error> {
self.active()?;
Ok(U256::from(take_weak!(self.miner).gas_floor_target())) Ok(U256::from(take_weak!(self.miner).gas_floor_target()))
} }
fn gas_ceil_target(&self) -> Result<U256, Error> { fn gas_ceil_target(&self) -> Result<U256, Error> {
self.active()?;
Ok(U256::from(take_weak!(self.miner).gas_ceil_target())) Ok(U256::from(take_weak!(self.miner).gas_ceil_target()))
} }
fn dev_logs(&self) -> Result<Vec<String>, Error> { fn dev_logs(&self) -> Result<Vec<String>, Error> {
self.active()?;
let logs = self.logger.logs(); let logs = self.logger.logs();
Ok(logs.as_slice().to_owned()) Ok(logs.as_slice().to_owned())
} }
fn dev_logs_levels(&self) -> Result<String, Error> { fn dev_logs_levels(&self) -> Result<String, Error> {
self.active()?;
Ok(self.logger.levels().to_owned()) Ok(self.logger.levels().to_owned())
} }
fn net_chain(&self) -> Result<String, Error> { fn net_chain(&self) -> Result<String, Error> {
self.active()?;
Ok(self.settings.chain.clone()) Ok(self.settings.chain.clone())
} }
fn net_peers(&self) -> Result<Peers, Error> { fn net_peers(&self) -> Result<Peers, Error> {
self.active()?;
let sync = take_weak!(self.sync); let sync = take_weak!(self.sync);
let sync_status = sync.status(); let sync_status = sync.status();
let net_config = take_weak!(self.net).network_config(); 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> { fn net_port(&self) -> Result<u16, Error> {
self.active()?;
Ok(self.settings.network_port) Ok(self.settings.network_port)
} }
fn node_name(&self) -> Result<String, Error> { fn node_name(&self) -> Result<String, Error> {
self.active()?;
Ok(self.settings.name.clone()) Ok(self.settings.name.clone())
} }
fn registry_address(&self) -> Result<Option<H160>, Error> { fn registry_address(&self) -> Result<Option<H160>, Error> {
self.active()?;
Ok( Ok(
take_weak!(self.client) take_weak!(self.client)
.additional_params() .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> { fn rpc_settings(&self) -> Result<RpcSettings, Error> {
self.active()?;
Ok(RpcSettings { Ok(RpcSettings {
enabled: self.settings.rpc_enabled, enabled: self.settings.rpc_enabled,
interface: self.settings.rpc_interface.clone(), 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> { fn default_extra_data(&self) -> Result<Bytes, Error> {
self.active()?;
Ok(Bytes::new(version_data())) Ok(Bytes::new(version_data()))
} }
fn gas_price_histogram(&self) -> Result<Histogram, Error> { 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) 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> { fn unsigned_transactions_count(&self) -> Result<usize, Error> {
self.active()?;
match self.signer { match self.signer {
None => Err(errors::signer_disabled()), None => Err(errors::signer_disabled()),
Some(ref signer) => Ok(signer.len()), 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> { fn generate_secret_phrase(&self) -> Result<String, Error> {
self.active()?;
Ok(random_phrase(12)) Ok(random_phrase(12))
} }
fn phrase_to_address(&self, phrase: String) -> Result<H160, Error> { fn phrase_to_address(&self, phrase: String) -> Result<H160, Error> {
self.active()?;
Ok(Brain::new(phrase).generate().unwrap().address().into()) 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> { 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) Ok(take_weak!(self.client)
.list_accounts(block_number.0.into(), after.map(Into::into).as_ref(), count) .list_accounts(block_number.0.into(), after.map(Into::into).as_ref(), count)
.map(|a| a.into_iter().map(Into::into).collect())) .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> { 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) Ok(take_weak!(self.client)
.list_storage(block_number.0.into(), &address.into(), after.map(Into::into).as_ref(), count) .list_storage(block_number.0.into(), &address.into(), after.map(Into::into).as_ref(), count)
.map(|a| a.into_iter().map(Into::into).collect())) .map(|a| a.into_iter().map(Into::into).collect()))
} }
fn encrypt_message(&self, key: H512, phrase: Bytes) -> Result<Bytes, Error> { fn encrypt_message(&self, key: H512, phrase: Bytes) -> Result<Bytes, Error> {
self.active()?;
ecies::encrypt(&key.into(), &DEFAULT_MAC, &phrase.0) ecies::encrypt(&key.into(), &DEFAULT_MAC, &phrase.0)
.map_err(errors::encryption_error) .map_err(errors::encryption_error)
.map(Into::into) .map(Into::into)
} }
fn pending_transactions(&self) -> Result<Vec<Transaction>, Error> { fn pending_transactions(&self) -> Result<Vec<Transaction>, Error> {
self.active()?;
Ok(take_weak!(self.miner).pending_transactions().into_iter().map(Into::into).collect::<Vec<_>>()) Ok(take_weak!(self.miner).pending_transactions().into_iter().map(Into::into).collect::<Vec<_>>())
} }
fn future_transactions(&self) -> Result<Vec<Transaction>, Error> { fn future_transactions(&self) -> Result<Vec<Transaction>, Error> {
self.active()?;
Ok(take_weak!(self.miner).future_transactions().into_iter().map(Into::into).collect::<Vec<_>>()) Ok(take_weak!(self.miner).future_transactions().into_iter().map(Into::into).collect::<Vec<_>>())
} }
fn pending_transactions_stats(&self) -> Result<BTreeMap<H256, TransactionStats>, Error> { fn pending_transactions_stats(&self) -> Result<BTreeMap<H256, TransactionStats>, Error> {
self.active()?;
let stats = take_weak!(self.sync).transactions_stats(); let stats = take_weak!(self.sync).transactions_stats();
Ok(stats.into_iter() Ok(stats.into_iter()
.map(|(hash, stats)| (hash.into(), stats.into())) .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> { fn local_transactions(&self) -> Result<BTreeMap<H256, LocalTransactionStatus>, Error> {
self.active()?;
let transactions = take_weak!(self.miner).local_transactions(); let transactions = take_weak!(self.miner).local_transactions();
Ok(transactions Ok(transactions
.into_iter() .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> { fn signer_port(&self) -> Result<u16, Error> {
self.active()?;
self.signer self.signer
.clone() .clone()
.and_then(|signer| signer.address()) .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> { fn dapps_port(&self) -> Result<u16, Error> {
self.active()?;
self.dapps_port self.dapps_port
.ok_or_else(|| errors::dapps_disabled()) .ok_or_else(|| errors::dapps_disabled())
} }
fn dapps_interface(&self) -> Result<String, Error> { fn dapps_interface(&self) -> Result<String, Error> {
self.active()?;
self.dapps_interface.clone() self.dapps_interface.clone()
.ok_or_else(|| errors::dapps_disabled()) .ok_or_else(|| errors::dapps_disabled())
} }
fn next_nonce(&self, address: H160) -> Result<U256, Error> { fn next_nonce(&self, address: H160) -> Result<U256, Error> {
self.active()?;
let address: Address = address.into(); let address: Address = address.into();
let miner = take_weak!(self.miner); let miner = take_weak!(self.miner);
let client = take_weak!(self.client); 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> { fn consensus_capability(&self) -> Result<ConsensusCapability, Error> {
self.active()?;
let updater = take_weak!(self.updater); let updater = take_weak!(self.updater);
Ok(updater.capability().into()) Ok(updater.capability().into())
} }
fn version_info(&self) -> Result<VersionInfo, Error> { fn version_info(&self) -> Result<VersionInfo, Error> {
self.active()?;
let updater = take_weak!(self.updater); let updater = take_weak!(self.updater);
Ok(updater.version_info().into()) Ok(updater.version_info().into())
} }
fn releases_info(&self) -> Result<Option<OperationsInfo>, Error> { fn releases_info(&self) -> Result<Option<OperationsInfo>, Error> {
self.active()?;
let updater = take_weak!(self.updater); let updater = take_weak!(self.updater);
Ok(updater.info().map(Into::into)) Ok(updater.info().map(Into::into))
} }
fn chain_status(&self) -> Result<ChainStatus, Error> { fn chain_status(&self) -> Result<ChainStatus, Error> {
self.active()?;
let chain_info = take_weak!(self.client).chain_info(); let chain_info = take_weak!(self.client).chain_info();
let gap = chain_info.ancient_block_number.map(|x| U256::from(x + 1)) let gap = chain_info.ancient_block_number.map(|x| U256::from(x + 1))

View File

@ -21,7 +21,6 @@ use util::{Address};
use ethkey::{Brain, Generator, Secret}; use ethkey::{Brain, Generator, Secret};
use ethcore::account_provider::AccountProvider; use ethcore::account_provider::AccountProvider;
use ethcore::client::MiningBlockChainClient;
use jsonrpc_core::Error; use jsonrpc_core::Error;
use v1::helpers::errors; use v1::helpers::errors;
@ -29,30 +28,21 @@ use v1::traits::ParityAccounts;
use v1::types::{H160 as RpcH160, H256 as RpcH256, DappId}; use v1::types::{H160 as RpcH160, H256 as RpcH256, DappId};
/// Account management (personal) rpc implementation. /// Account management (personal) rpc implementation.
pub struct ParityAccountsClient<C> where C: MiningBlockChainClient { pub struct ParityAccountsClient {
accounts: Weak<AccountProvider>, accounts: Weak<AccountProvider>,
client: Weak<C>,
} }
impl<C> ParityAccountsClient<C> where C: MiningBlockChainClient { impl ParityAccountsClient {
/// Creates new PersonalClient /// Creates new PersonalClient
pub fn new(store: &Arc<AccountProvider>, client: &Arc<C>) -> Self { pub fn new(store: &Arc<AccountProvider>) -> Self {
ParityAccountsClient { ParityAccountsClient {
accounts: Arc::downgrade(store), 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> { fn all_accounts_info(&self) -> Result<BTreeMap<RpcH160, BTreeMap<String, String>>, Error> {
self.active()?;
let store = take_weak!(self.accounts); let store = take_weak!(self.accounts);
let info = store.accounts_info().map_err(|e| errors::account("Could not fetch account info.", e))?; let info = store.accounts_info().map_err(|e| errors::account("Could not fetch account info.", e))?;
let other = store.addresses_info(); 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> { fn new_account_from_phrase(&self, phrase: String, pass: String) -> Result<RpcH160, Error> {
self.active()?;
let store = take_weak!(self.accounts); let store = take_weak!(self.accounts);
let brain = Brain::new(phrase).generate().unwrap(); 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> { fn new_account_from_wallet(&self, json: String, pass: String) -> Result<RpcH160, Error> {
self.active()?;
let store = take_weak!(self.accounts); let store = take_weak!(self.accounts);
store.import_presale(json.as_bytes(), &pass) 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> { fn new_account_from_secret(&self, secret: RpcH256, pass: String) -> Result<RpcH160, Error> {
self.active()?;
let store = take_weak!(self.accounts); let store = take_weak!(self.accounts);
let secret = Secret::from_slice(&secret.0) 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> { fn test_password(&self, account: RpcH160, password: String) -> Result<bool, Error> {
self.active()?;
let account: Address = account.into(); let account: Address = account.into();
take_weak!(self.accounts) 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> { fn change_password(&self, account: RpcH160, password: String, new_password: String) -> Result<bool, Error> {
self.active()?;
let account: Address = account.into(); let account: Address = account.into();
take_weak!(self.accounts) take_weak!(self.accounts)
.change_password(&account, password, new_password) .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> { fn kill_account(&self, account: RpcH160, password: String) -> Result<bool, Error> {
self.active()?;
let account: Address = account.into(); let account: Address = account.into();
take_weak!(self.accounts) take_weak!(self.accounts)
.kill_account(&account, &password) .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> { fn remove_address(&self, addr: RpcH160) -> Result<bool, Error> {
self.active()?;
let store = take_weak!(self.accounts); let store = take_weak!(self.accounts);
let addr: Address = addr.into(); 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> { fn set_account_name(&self, addr: RpcH160, name: String) -> Result<bool, Error> {
self.active()?;
let store = take_weak!(self.accounts); let store = take_weak!(self.accounts);
let addr: Address = addr.into(); 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> { fn set_account_meta(&self, addr: RpcH160, meta: String) -> Result<bool, Error> {
self.active()?;
let store = take_weak!(self.accounts); let store = take_weak!(self.accounts);
let addr: Address = addr.into(); 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> { fn geth_accounts(&self) -> Result<Vec<RpcH160>, Error> {
self.active()?;
let store = take_weak!(self.accounts); let store = take_weak!(self.accounts);
Ok(into_vec(store.list_geth_accounts(false))) Ok(into_vec(store.list_geth_accounts(false)))

View File

@ -23,7 +23,7 @@ use ethcore::client::MiningBlockChainClient;
use ethcore::mode::Mode; use ethcore::mode::Mode;
use ethsync::ManageNetwork; use ethsync::ManageNetwork;
use fetch::{self, Fetch}; use fetch::{self, Fetch};
use futures::{self, BoxFuture, Future}; use futures::{BoxFuture, Future};
use util::sha3; use util::sha3;
use updater::{Service as UpdateService}; use updater::{Service as UpdateService};
@ -62,12 +62,6 @@ impl<C, M, U, F> ParitySetClient<C, M, U, F> where
fetch: fetch, 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 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> { 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()); take_weak!(self.miner).set_minimal_gas_price(gas_price.into());
Ok(true) Ok(true)
} }
fn set_gas_floor_target(&self, target: U256) -> Result<bool, Error> { fn set_gas_floor_target(&self, target: U256) -> Result<bool, Error> {
self.active()?;
take_weak!(self.miner).set_gas_floor_target(target.into()); take_weak!(self.miner).set_gas_floor_target(target.into());
Ok(true) Ok(true)
} }
fn set_gas_ceil_target(&self, target: U256) -> Result<bool, Error> { fn set_gas_ceil_target(&self, target: U256) -> Result<bool, Error> {
self.active()?;
take_weak!(self.miner).set_gas_ceil_target(target.into()); take_weak!(self.miner).set_gas_ceil_target(target.into());
Ok(true) Ok(true)
} }
fn set_extra_data(&self, extra_data: Bytes) -> Result<bool, Error> { fn set_extra_data(&self, extra_data: Bytes) -> Result<bool, Error> {
self.active()?;
take_weak!(self.miner).set_extra_data(extra_data.into_vec()); take_weak!(self.miner).set_extra_data(extra_data.into_vec());
Ok(true) Ok(true)
} }
fn set_author(&self, author: H160) -> Result<bool, Error> { fn set_author(&self, author: H160) -> Result<bool, Error> {
self.active()?;
take_weak!(self.miner).set_author(author.into()); take_weak!(self.miner).set_author(author.into());
Ok(true) Ok(true)
} }
fn set_engine_signer(&self, address: H160, password: String) -> Result<bool, Error> { 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)?; take_weak!(self.miner).set_engine_signer(address.into(), password).map_err(Into::into).map_err(errors::from_password_error)?;
Ok(true) Ok(true)
} }
fn set_transactions_limit(&self, limit: usize) -> Result<bool, Error> { fn set_transactions_limit(&self, limit: usize) -> Result<bool, Error> {
self.active()?;
take_weak!(self.miner).set_transactions_limit(limit); take_weak!(self.miner).set_transactions_limit(limit);
Ok(true) Ok(true)
} }
fn set_tx_gas_limit(&self, limit: U256) -> Result<bool, Error> { fn set_tx_gas_limit(&self, limit: U256) -> Result<bool, Error> {
self.active()?;
take_weak!(self.miner).set_tx_gas_limit(limit.into()); take_weak!(self.miner).set_tx_gas_limit(limit.into());
Ok(true) Ok(true)
} }
fn add_reserved_peer(&self, peer: String) -> Result<bool, Error> { fn add_reserved_peer(&self, peer: String) -> Result<bool, Error> {
self.active()?;
match take_weak!(self.net).add_reserved_peer(peer) { match take_weak!(self.net).add_reserved_peer(peer) {
Ok(()) => Ok(true), Ok(()) => Ok(true),
Err(e) => Err(errors::invalid_params("Peer address", e)), 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> { fn remove_reserved_peer(&self, peer: String) -> Result<bool, Error> {
self.active()?;
match take_weak!(self.net).remove_reserved_peer(peer) { match take_weak!(self.net).remove_reserved_peer(peer) {
Ok(()) => Ok(true), Ok(()) => Ok(true),
Err(e) => Err(errors::invalid_params("Peer address", e)), 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> { fn drop_non_reserved_peers(&self) -> Result<bool, Error> {
self.active()?;
take_weak!(self.net).deny_unreserved_peers(); take_weak!(self.net).deny_unreserved_peers();
Ok(true) Ok(true)
} }
fn accept_non_reserved_peers(&self) -> Result<bool, Error> { fn accept_non_reserved_peers(&self) -> Result<bool, Error> {
self.active()?;
take_weak!(self.net).accept_unreserved_peers(); take_weak!(self.net).accept_unreserved_peers();
Ok(true) 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> { 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| { self.fetch.process(self.fetch.fetch(&url).then(move |result| {
result result
.map_err(errors::from_fetch_error) .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> { fn upgrade_ready(&self) -> Result<Option<ReleaseInfo>, Error> {
self.active()?;
let updater = take_weak!(self.updater); let updater = take_weak!(self.updater);
Ok(updater.upgrade_ready().map(Into::into)) Ok(updater.upgrade_ready().map(Into::into))
} }
fn execute_upgrade(&self) -> Result<bool, Error> { fn execute_upgrade(&self) -> Result<bool, Error> {
self.active()?;
let updater = take_weak!(self.updater); let updater = take_weak!(self.updater);
Ok(updater.execute_upgrade()) Ok(updater.execute_upgrade())
} }

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