Merge branch 'cht-td' into lightrpc
This commit is contained in:
commit
484b93abdc
114
Cargo.lock
generated
114
Cargo.lock
generated
@ -73,6 +73,11 @@ name = "ansi_term"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "antidote"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "app_dirs"
|
||||
version = "1.1.1"
|
||||
@ -285,6 +290,16 @@ name = "dtoa"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "elastic-array"
|
||||
version = "0.6.0"
|
||||
@ -515,6 +530,7 @@ dependencies = [
|
||||
"ethcore-network 1.6.0",
|
||||
"ethcore-util 1.6.0",
|
||||
"futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itertools 0.5.9 (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)",
|
||||
"rlp 0.1.0",
|
||||
@ -650,7 +666,7 @@ dependencies = [
|
||||
"ethcore-bloom-journal 0.1.0",
|
||||
"ethcore-devtools 1.6.0",
|
||||
"heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itertools 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@ -715,7 +731,7 @@ dependencies = [
|
||||
"ethcore-util 1.6.0",
|
||||
"ethcrypto 0.1.0",
|
||||
"ethkey 0.2.0",
|
||||
"itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itertools 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@ -779,7 +795,8 @@ dependencies = [
|
||||
"futures-cpupool 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"reqwest 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"reqwest 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -896,6 +913,35 @@ dependencies = [
|
||||
"vecio 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num_cpus 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"traitobject 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"typeable 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)",
|
||||
"url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-native-tls"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hyper 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"native-tls 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.1.0"
|
||||
@ -941,14 +987,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.4.13"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"either 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "jsonrpc-core"
|
||||
version = "5.0.0"
|
||||
@ -1509,7 +1563,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "parity-ui-precompiled"
|
||||
version = "1.4.0"
|
||||
source = "git+https://github.com/ethcore/js-precompiled.git#cb8dc7c61dd7976f062863182a1c6d77ba319a36"
|
||||
source = "git+https://github.com/ethcore/js-precompiled.git#416d00db677b8219f7548bb4dfa2f25c4b19f36e"
|
||||
dependencies = [
|
||||
"parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@ -1707,15 +1761,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.2.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"hyper 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hyper 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hyper-native-tls 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"native-tls 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_urlencoded 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_urlencoded 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@ -1901,6 +1955,11 @@ name = "serde"
|
||||
version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "serde_codegen"
|
||||
version = "0.8.19"
|
||||
@ -1933,11 +1992,24 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.3.0"
|
||||
name = "serde_json"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"dtoa 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itoa 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"dtoa 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itoa 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@ -2397,6 +2469,7 @@ dependencies = [
|
||||
"checksum advapi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e06588080cb19d0acb6739808aafa5f26bfb2ca015b2b6370028b44cf7cb8a9a"
|
||||
"checksum aho-corasick 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "67077478f0a03952bed2e6786338d400d40c25e9836e08ad50af96607317fd03"
|
||||
"checksum ansi_term 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1f46cd5b1d660c938e3f92dfe7a73d832b3281479363dd0cd9c1c2fbf60f7962"
|
||||
"checksum antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "34fde25430d87a9388dadbe6e34d7f72a462c8b43ac8d309b42b0a8505d7e2a5"
|
||||
"checksum app_dirs 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b7d1c0d48a81bbb13043847f957971f4d87c81542d80ece5e84ba3cba4058fd4"
|
||||
"checksum arrayvec 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "16e3bdb2f54b3ace0285975d59a97cf8ed3855294b2b6bc651fcf22a9c352975"
|
||||
"checksum aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07d344974f0a155f091948aa389fb1b912d3a58414fbdb9c8d446d193ee3496a"
|
||||
@ -2425,6 +2498,8 @@ dependencies = [
|
||||
"checksum deque 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1614659040e711785ed8ea24219140654da1729f3ec8a47a9719d041112fe7bf"
|
||||
"checksum docopt 0.6.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4cc0acb4ce0828c6a5a11d47baa432fe885881c27428c3a4e473e454ffe57a76"
|
||||
"checksum dtoa 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0dd841b58510c9618291ffa448da2e4e0f699d984d436122372f446dae62263d"
|
||||
"checksum dtoa 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5edd69c67b2f8e0911629b7e6b8a34cb3956613cd7c6e6414966dee349c2db4f"
|
||||
"checksum either 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2b503c86dad62aaf414ecf2b8c527439abedb3f8d812537f0b12bfd6f32a91"
|
||||
"checksum elastic-array 0.6.0 (git+https://github.com/ethcore/elastic-array)" = "<none>"
|
||||
"checksum env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "aba65b63ffcc17ffacd6cf5aa843da7c5a25e3bd4bbe0b7def8b214e411250e5"
|
||||
"checksum eth-secp256k1 0.5.4 (git+https://github.com/ethcore/rust-secp256k1)" = "<none>"
|
||||
@ -2441,12 +2516,15 @@ dependencies = [
|
||||
"checksum hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2da7d3a34cf6406d9d700111b8eafafe9a251de41ae71d8052748259343b58"
|
||||
"checksum httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "46534074dbb80b070d60a5cb8ecadd8963a00a438ae1a95268850a7ef73b67ae"
|
||||
"checksum hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)" = "<none>"
|
||||
"checksum hyper 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)" = "220407e5a263f110ec30a071787c9535918fdfc97def5680c90013c3f30c38c1"
|
||||
"checksum hyper 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)" = "bcb3fc65554155980167fb821d05c7c66177f92464976c0b676a19d9e03387a7"
|
||||
"checksum hyper-native-tls 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "afe68f772f0497a7205e751626bb8e1718568b58534b6108c73a74ef80483409"
|
||||
"checksum idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1053236e00ce4f668aeca4a769a09b3bf5a682d802abd6f3cb39374f6b162c11"
|
||||
"checksum igd 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c8c12b1795b8b168f577c45fa10379b3814dcb11b7ab702406001f0d63f40484"
|
||||
"checksum isatty 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7408a548dc0e406b7912d9f84c261cc533c1866e047644a811c133c56041ac0c"
|
||||
"checksum itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)" = "086e1fa5fe48840b1cfdef3a20c7e3115599f8d5c4c87ef32a794a7cdd184d76"
|
||||
"checksum itertools 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)" = "d95557e7ba6b71377b0f2c3b3ae96c53f1b75a926a6901a500f557a370af730a"
|
||||
"checksum itoa 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ae3088ea4baeceb0284ee9eea42f591226e6beaecf65373e41b38d95a1b8e7a1"
|
||||
"checksum itoa 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "91fd9dc2c587067de817fec4ad355e3818c3d893a78cab32a0a474c7a15bb8d5"
|
||||
"checksum jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>"
|
||||
"checksum jsonrpc-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>"
|
||||
@ -2521,7 +2599,7 @@ dependencies = [
|
||||
"checksum rayon 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "655df67c314c30fa3055a365eae276eb88aa4f3413a352a1ab32c1320eda41ea"
|
||||
"checksum regex 0.1.68 (registry+https://github.com/rust-lang/crates.io-index)" = "b4329b8928a284580a1c63ec9d846b12f6d3472317243ff7077aff11f23f2b29"
|
||||
"checksum regex-syntax 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "841591b1e05609a643e3b4d0045fce04f701daba7151ddcd3ad47b080693d5a9"
|
||||
"checksum reqwest 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "83186fee0d4dbeb95e610b77b05b05cf5b31703dd375222acb74c3dff4be957c"
|
||||
"checksum reqwest 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bef9ed8fdfcc30947d6b774938dc0c3f369a474efe440df2c7f278180b2d2e6"
|
||||
"checksum rocksdb 0.4.5 (git+https://github.com/ethcore/rust-rocksdb)" = "<none>"
|
||||
"checksum rocksdb-sys 0.3.0 (git+https://github.com/ethcore/rust-rocksdb)" = "<none>"
|
||||
"checksum rotor 0.6.3 (git+https://github.com/ethcore/rotor)" = "<none>"
|
||||
@ -2540,10 +2618,12 @@ dependencies = [
|
||||
"checksum semver 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ae2ff60ecdb19c255841c066cbfa5f8c2a4ada1eb3ae47c77ab6667128da71f5"
|
||||
"checksum semver-parser 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e88e43a5a74dd2a11707f9c21dfd4a423c66bd871df813227bb0a3e78f3a1ae9"
|
||||
"checksum serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)" = "58a19c0871c298847e6b68318484685cd51fa5478c0c905095647540031356e5"
|
||||
"checksum serde 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4d8f810025e9d09c4eaa49c16eaf878f34a947889e878cd7d3b5bef3197cc119"
|
||||
"checksum serde_codegen 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)" = "ce29a6ae259579707650ec292199b5fed2c0b8e2a4bdc994452d24d1bcf2242a"
|
||||
"checksum serde_codegen_internals 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "59933a62554548c690d2673c5164f0c4a46be7c5731edfd94b0ecb1048940732"
|
||||
"checksum serde_json 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3f7d3c184d35801fb8b32b46a7d58d57dbcc150b0eb2b46a1eb79645e8ecfd5b"
|
||||
"checksum serde_urlencoded 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "53d4ebaa8d1d4f90d1b63dfca81ccd98ac20e1e479dbae393cbaf60f6fecd8d8"
|
||||
"checksum serde_json 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)" = "fea48f4d4df4e620e3c81fd2bf28c93dd0d266361a76bac4f254b71f0e13f3cd"
|
||||
"checksum serde_urlencoded 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a81f15da4b9780e1524697f73b09076b6e42298ef673bead9ca8f848b334ef84"
|
||||
"checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c"
|
||||
"checksum shell32-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "72f20b8f3c060374edb8046591ba28f62448c369ccbdc7b02075103fb3a9e38d"
|
||||
"checksum siphasher 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5c44e42fa187b5a8782489cf7740cc27c3125806be2bf33563cf5e02e9533fcd"
|
||||
|
@ -22,6 +22,7 @@ time = "0.1"
|
||||
smallvec = "0.3.1"
|
||||
futures = "0.1"
|
||||
rand = "0.3"
|
||||
itertools = "0.5"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
@ -12,10 +12,130 @@
|
||||
// GNU General Public License for more details.
|
||||
|
||||
//! Canonical hash trie definitions and helper functions.
|
||||
//!
|
||||
//! Each CHT is a trie mapping block numbers to canonical hashes and total difficulty.
|
||||
//! One is generated for every `SIZE` blocks, allowing us to discard those blocks in
|
||||
//! favor the the trie root. When the "ancient" blocks need to be accessed, we simply
|
||||
//! request an inclusion proof of a specific block number against the trie with the
|
||||
//! root has. A correct proof implies that the claimed block is identical to the one
|
||||
//! we discarded.
|
||||
|
||||
use ethcore::ids::BlockId;
|
||||
use util::{Bytes, H256, U256, HashDB, MemoryDB};
|
||||
use util::trie::{self, TrieMut, TrieDBMut, Trie, TrieDB, Recorder};
|
||||
use rlp::{Stream, RlpStream};
|
||||
|
||||
// encode a key.
|
||||
macro_rules! key {
|
||||
($num: expr) => { ::rlp::encode(&$num) }
|
||||
}
|
||||
|
||||
macro_rules! val {
|
||||
($hash: expr, $td: expr) => {{
|
||||
let mut stream = RlpStream::new_list(2);
|
||||
stream.append(&$hash).append(&$td);
|
||||
stream.drain()
|
||||
}}
|
||||
}
|
||||
|
||||
/// The size of each CHT.
|
||||
pub const SIZE: u64 = 2048;
|
||||
|
||||
/// A canonical hash trie. This is generic over any database it can query.
|
||||
/// See module docs for more details.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CHT<DB: HashDB> {
|
||||
db: DB,
|
||||
root: H256, // the root of this CHT.
|
||||
number: u64,
|
||||
}
|
||||
|
||||
impl<DB: HashDB> CHT<DB> {
|
||||
/// Query the root of the CHT.
|
||||
pub fn root(&self) -> H256 { self.root }
|
||||
|
||||
/// Query the number of the CHT.
|
||||
pub fn number(&self) -> u64 { self.number }
|
||||
|
||||
/// Generate an inclusion proof for the entry at a specific block.
|
||||
/// Nodes before level `from_level` will be omitted.
|
||||
/// Returns an error on an incomplete trie, and `Ok(None)` on an unprovable request.
|
||||
pub fn prove(&self, num: u64, from_level: u32) -> trie::Result<Option<Vec<Bytes>>> {
|
||||
if block_to_cht_number(num) != Some(self.number) { return Ok(None) }
|
||||
|
||||
let mut recorder = Recorder::with_depth(from_level);
|
||||
let t = TrieDB::new(&self.db, &self.root)?;
|
||||
t.get_with(&key!(num), &mut recorder)?;
|
||||
|
||||
Ok(Some(recorder.drain().into_iter().map(|x| x.data).collect()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Block information necessary to build a CHT.
|
||||
pub struct BlockInfo {
|
||||
/// The block's hash.
|
||||
pub hash: H256,
|
||||
/// The block's parent's hash.
|
||||
pub parent_hash: H256,
|
||||
/// The block's total difficulty.
|
||||
pub total_difficulty: U256,
|
||||
}
|
||||
|
||||
/// Build an in-memory CHT from a closure which provides necessary information
|
||||
/// about blocks. If the fetcher ever fails to provide the info, the CHT
|
||||
/// will not be generated.
|
||||
pub fn build<F>(cht_num: u64, mut fetcher: F) -> Option<CHT<MemoryDB>>
|
||||
where F: FnMut(BlockId) -> Option<BlockInfo>
|
||||
{
|
||||
let mut db = MemoryDB::new();
|
||||
|
||||
// start from the last block by number and work backwards.
|
||||
let last_num = start_number(cht_num + 1) - 1;
|
||||
let mut id = BlockId::Number(last_num);
|
||||
|
||||
let mut root = H256::default();
|
||||
|
||||
{
|
||||
let mut t = TrieDBMut::new(&mut db, &mut root);
|
||||
for blk_num in (0..SIZE).map(|n| last_num - n) {
|
||||
let info = match fetcher(id) {
|
||||
Some(info) => info,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
id = BlockId::Hash(info.parent_hash);
|
||||
t.insert(&key!(blk_num), &val!(info.hash, info.total_difficulty))
|
||||
.expect("fresh in-memory database is infallible; qed");
|
||||
}
|
||||
}
|
||||
|
||||
Some(CHT {
|
||||
db: db,
|
||||
root: root,
|
||||
number: cht_num,
|
||||
})
|
||||
}
|
||||
|
||||
/// Compute a CHT root from an iterator of (hash, td) pairs. Fails if shorter than
|
||||
/// SIZE items. The items are assumed to proceed sequentially from `start_number(cht_num)`.
|
||||
/// Discards the trie's nodes.
|
||||
pub fn compute_root<I>(cht_num: u64, iterable: I) -> Option<H256>
|
||||
where I: IntoIterator<Item=(H256, U256)>
|
||||
{
|
||||
let mut v = Vec::with_capacity(SIZE as usize);
|
||||
let start_num = start_number(cht_num) as usize;
|
||||
|
||||
for (i, (h, td)) in iterable.into_iter().take(SIZE as usize).enumerate() {
|
||||
v.push((key!(i + start_num).to_vec(), val!(h, td).to_vec()))
|
||||
}
|
||||
|
||||
if v.len() == SIZE as usize {
|
||||
Some(::util::triehash::trie_root(v))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a block number to a CHT number.
|
||||
/// Returns `None` for `block_num` == 0, `Some` otherwise.
|
||||
pub fn block_to_cht_number(block_num: u64) -> Option<u64> {
|
||||
@ -37,6 +157,12 @@ pub fn start_number(cht_num: u64) -> u64 {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn size_is_lt_usize() {
|
||||
// to ensure safe casting on the target platform.
|
||||
assert!(::cht::SIZE < usize::max_value() as u64)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_to_cht_number() {
|
||||
assert!(::cht::block_to_cht_number(0).is_none());
|
||||
|
@ -173,26 +173,34 @@ impl HeaderChain {
|
||||
// produce next CHT root if it's time.
|
||||
let earliest_era = *candidates.keys().next().expect("at least one era just created; qed");
|
||||
if earliest_era + HISTORY + cht::SIZE <= number {
|
||||
let mut values = Vec::with_capacity(cht::SIZE as usize);
|
||||
{
|
||||
let cht_num = cht::block_to_cht_number(earliest_era)
|
||||
.expect("fails only for number == 0; genesis never imported; qed");
|
||||
debug_assert_eq!(cht_num as usize, self.cht_roots.lock().len());
|
||||
|
||||
let mut headers = self.headers.write();
|
||||
for i in (0..cht::SIZE).map(|x| x + earliest_era) {
|
||||
|
||||
let cht_root = {
|
||||
let mut i = earliest_era;
|
||||
|
||||
// iterable function which removes the candidates as it goes
|
||||
// along. this will only be called until the CHT is complete.
|
||||
let iter = || {
|
||||
let era_entry = candidates.remove(&i)
|
||||
.expect("all eras are sequential with no gaps; qed");
|
||||
i += 1;
|
||||
|
||||
for ancient in &era_entry.candidates {
|
||||
headers.remove(&ancient.hash);
|
||||
}
|
||||
|
||||
values.push((
|
||||
::rlp::encode(&i).to_vec(),
|
||||
::rlp::encode(&era_entry.canonical_hash).to_vec(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let canon = &era_entry.candidates[0];
|
||||
(canon.hash, canon.total_difficulty)
|
||||
};
|
||||
cht::compute_root(cht_num, ::itertools::repeat_call(iter))
|
||||
.expect("fails only when too few items; this is checked; qed")
|
||||
};
|
||||
|
||||
let cht_root = ::util::triehash::trie_root(values);
|
||||
debug!(target: "chain", "Produced CHT {} root: {:?}", (earliest_era - 1) % cht::SIZE, cht_root);
|
||||
debug!(target: "chain", "Produced CHT {} root: {:?}", cht_num, cht_root);
|
||||
|
||||
self.cht_roots.lock().push(cht_root);
|
||||
}
|
||||
|
@ -21,8 +21,9 @@ use ethcore::block_status::BlockStatus;
|
||||
use ethcore::client::ClientReport;
|
||||
use ethcore::ids::BlockId;
|
||||
use ethcore::header::Header;
|
||||
use ethcore::views::HeaderView;
|
||||
use ethcore::verification::queue::{self, HeaderQueue};
|
||||
use ethcore::transaction::PendingTransaction;
|
||||
use ethcore::transaction::{PendingTransaction, Condition as TransactionCondition};
|
||||
use ethcore::blockchain_info::BlockChainInfo;
|
||||
use ethcore::spec::Spec;
|
||||
use ethcore::service::ClientIoMessage;
|
||||
@ -34,6 +35,7 @@ use util::{Bytes, Mutex, RwLock};
|
||||
|
||||
use provider::Provider;
|
||||
use request;
|
||||
use time;
|
||||
|
||||
use self::header_chain::HeaderChain;
|
||||
|
||||
@ -110,7 +112,11 @@ impl Client {
|
||||
let best_num = self.chain.best_block().number;
|
||||
self.tx_pool.lock()
|
||||
.values()
|
||||
.filter(|t| t.min_block.as_ref().map_or(true, |x| x <= &best_num))
|
||||
.filter(|t| match t.condition {
|
||||
Some(TransactionCondition::Number(ref x)) => x <= &best_num,
|
||||
Some(TransactionCondition::Timestamp(ref x)) => *x <= time::get_time().sec as u64,
|
||||
None => true,
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
@ -135,6 +141,7 @@ impl Client {
|
||||
genesis_hash: genesis_hash,
|
||||
best_block_hash: best_block.hash,
|
||||
best_block_number: best_block.number,
|
||||
best_block_timestamp: HeaderView::new(&self.chain.get_header(BlockId::Latest).expect("Latest hash is always in the chain")).timestamp(),
|
||||
ancient_block_hash: if first_block.is_some() { Some(genesis_hash) } else { None },
|
||||
ancient_block_number: if first_block.is_some() { Some(0) } else { None },
|
||||
first_block_hash: first_block.as_ref().map(|first| first.hash),
|
||||
|
@ -68,6 +68,7 @@ extern crate smallvec;
|
||||
extern crate time;
|
||||
extern crate futures;
|
||||
extern crate rand;
|
||||
extern crate itertools;
|
||||
|
||||
#[cfg(feature = "ipc")]
|
||||
extern crate ethcore_ipc as ipc;
|
||||
|
@ -23,6 +23,8 @@ use ethcore::transaction::PendingTransaction;
|
||||
use ethcore::ids::BlockId;
|
||||
use ethcore::encoded;
|
||||
|
||||
use cht::{self, BlockInfo};
|
||||
|
||||
use util::{Bytes, H256};
|
||||
|
||||
use request;
|
||||
@ -227,48 +229,54 @@ impl<T: ProvingBlockChainClient + ?Sized> Provider for T {
|
||||
}
|
||||
|
||||
fn header_proof(&self, req: request::HeaderProof) -> Option<(encoded::Header, Vec<Bytes>)> {
|
||||
use util::MemoryDB;
|
||||
use util::trie::{Trie, TrieMut, TrieDB, TrieDBMut, Recorder};
|
||||
|
||||
if Some(req.cht_number) != ::cht::block_to_cht_number(req.block_number) {
|
||||
if Some(req.cht_number) != cht::block_to_cht_number(req.block_number) {
|
||||
debug!(target: "les_provider", "Requested CHT number mismatch with block number.");
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut memdb = MemoryDB::new();
|
||||
let mut root = H256::default();
|
||||
let mut needed_hdr = None;
|
||||
{
|
||||
let mut t = TrieDBMut::new(&mut memdb, &mut root);
|
||||
let start_num = ::cht::start_number(req.cht_number);
|
||||
for i in (0..::cht::SIZE).map(|x| x + start_num) {
|
||||
match self.block_header(BlockId::Number(i)) {
|
||||
None => return None,
|
||||
Some(hdr) => {
|
||||
t.insert(
|
||||
&*::rlp::encode(&i),
|
||||
&*::rlp::encode(&hdr.hash()),
|
||||
).expect("fresh in-memory database is infallible; qed");
|
||||
|
||||
if i == req.block_number { needed_hdr = Some(hdr) }
|
||||
// build the CHT, caching the requested header as we pass through it.
|
||||
let cht = {
|
||||
let block_info = |id| {
|
||||
let hdr = self.block_header(id);
|
||||
let td = self.block_total_difficulty(id);
|
||||
|
||||
match (hdr, td) {
|
||||
(Some(hdr), Some(td)) => {
|
||||
let info = BlockInfo {
|
||||
hash: hdr.hash(),
|
||||
parent_hash: hdr.parent_hash(),
|
||||
total_difficulty: td,
|
||||
};
|
||||
|
||||
if hdr.number() == req.block_number {
|
||||
needed_hdr = Some(hdr);
|
||||
}
|
||||
|
||||
Some(info)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
match cht::build(req.cht_number, block_info) {
|
||||
Some(cht) => cht,
|
||||
None => return None, // incomplete CHT.
|
||||
}
|
||||
};
|
||||
|
||||
let needed_hdr = needed_hdr.expect("`needed_hdr` always set in loop, number checked before; qed");
|
||||
|
||||
let mut recorder = Recorder::with_depth(req.from_level);
|
||||
let t = TrieDB::new(&memdb, &root)
|
||||
.expect("Same DB and root as just produced by TrieDBMut; qed");
|
||||
|
||||
if let Err(e) = t.get_with(&*::rlp::encode(&req.block_number), &mut recorder) {
|
||||
// prove our result.
|
||||
match cht.prove(req.block_number, req.from_level) {
|
||||
Ok(Some(proof)) => Some((needed_hdr, proof)),
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
debug!(target: "les_provider", "Error looking up number in freshly-created CHT: {}", e);
|
||||
return None;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: cache calculated CHT if possible.
|
||||
let proof = recorder.drain().into_iter().map(|x| x.data).collect();
|
||||
Some((needed_hdr, proof))
|
||||
}
|
||||
|
||||
fn ready_transactions(&self) -> Vec<PendingTransaction> {
|
||||
|
@ -9,7 +9,11 @@
|
||||
"0x82a978b3f5962a5b0957d9ee9eef472ee55b42f1",
|
||||
"0x7d577a597b2742b498cb5cf0c26cdcd726d39e6e"
|
||||
]
|
||||
}
|
||||
},
|
||||
"timeoutPropose": 10000,
|
||||
"timeoutPrevote": 10000,
|
||||
"timeoutPrecommit": 10000,
|
||||
"timeoutCommit": 10000
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -21,7 +21,7 @@ mod stores;
|
||||
use self::stores::{AddressBook, DappsSettingsStore, NewDappsPolicy};
|
||||
|
||||
use std::fmt;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::time::{Instant, Duration};
|
||||
use util::RwLock;
|
||||
use ethstore::{SimpleSecretStore, SecretStore, Error as SSError, EthStore, EthMultiStore,
|
||||
@ -114,8 +114,8 @@ impl AccountProvider {
|
||||
pub fn new(sstore: Box<SecretStore>) -> Self {
|
||||
AccountProvider {
|
||||
unlocked: RwLock::new(HashMap::new()),
|
||||
address_book: RwLock::new(AddressBook::new(sstore.local_path().into())),
|
||||
dapps_settings: RwLock::new(DappsSettingsStore::new(sstore.local_path().into())),
|
||||
address_book: RwLock::new(AddressBook::new(&sstore.local_path())),
|
||||
dapps_settings: RwLock::new(DappsSettingsStore::new(&sstore.local_path())),
|
||||
sstore: sstore,
|
||||
transient_sstore: transient_sstore(),
|
||||
}
|
||||
@ -216,7 +216,7 @@ impl AccountProvider {
|
||||
Some(accounts) => Ok(accounts),
|
||||
None => match dapps.policy() {
|
||||
NewDappsPolicy::AllAccounts => self.accounts(),
|
||||
NewDappsPolicy::Whitelist(accounts) => Ok(accounts),
|
||||
NewDappsPolicy::Whitelist(accounts) => self.filter_addresses(accounts),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -231,28 +231,42 @@ impl AccountProvider {
|
||||
|
||||
/// Sets addresses visile for dapp.
|
||||
pub fn set_dapps_addresses(&self, dapp: DappId, addresses: Vec<Address>) -> Result<(), Error> {
|
||||
let addresses = self.filter_addresses(addresses)?;
|
||||
self.dapps_settings.write().set_accounts(dapp, addresses);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns each address along with metadata.
|
||||
pub fn addresses_info(&self) -> Result<HashMap<Address, AccountMeta>, Error> {
|
||||
Ok(self.address_book.read().get())
|
||||
/// Removes addresses that are neither accounts nor in address book.
|
||||
fn filter_addresses(&self, addresses: Vec<Address>) -> Result<Vec<Address>, Error> {
|
||||
let valid = self.addresses_info().into_iter()
|
||||
.map(|(address, _)| address)
|
||||
.chain(self.accounts()?)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
Ok(addresses.into_iter()
|
||||
.filter(|a| valid.contains(&a))
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns each address along with metadata.
|
||||
pub fn set_address_name(&self, account: Address, name: String) -> Result<(), Error> {
|
||||
Ok(self.address_book.write().set_name(account, name))
|
||||
pub fn addresses_info(&self) -> HashMap<Address, AccountMeta> {
|
||||
self.address_book.read().get()
|
||||
}
|
||||
|
||||
/// Returns each address along with metadata.
|
||||
pub fn set_address_meta(&self, account: Address, meta: String) -> Result<(), Error> {
|
||||
Ok(self.address_book.write().set_meta(account, meta))
|
||||
pub fn set_address_name(&self, account: Address, name: String) {
|
||||
self.address_book.write().set_name(account, name)
|
||||
}
|
||||
|
||||
/// Returns each address along with metadata.
|
||||
pub fn set_address_meta(&self, account: Address, meta: String) {
|
||||
self.address_book.write().set_meta(account, meta)
|
||||
}
|
||||
|
||||
/// Removes and address from the addressbook
|
||||
pub fn remove_address(&self, addr: Address) -> Result<(), Error> {
|
||||
Ok(self.address_book.write().remove(addr))
|
||||
pub fn remove_address(&self, addr: Address) {
|
||||
self.address_book.write().remove(addr)
|
||||
}
|
||||
|
||||
/// Returns each account along with name and meta.
|
||||
@ -502,9 +516,12 @@ mod tests {
|
||||
let app = DappId("app1".into());
|
||||
// set `AllAccounts` policy
|
||||
ap.set_new_dapps_whitelist(None).unwrap();
|
||||
// add accounts to address book
|
||||
ap.set_address_name(1.into(), "1".into());
|
||||
ap.set_address_name(2.into(), "2".into());
|
||||
|
||||
// when
|
||||
ap.set_dapps_addresses(app.clone(), vec![1.into(), 2.into()]).unwrap();
|
||||
ap.set_dapps_addresses(app.clone(), vec![1.into(), 2.into(), 3.into()]).unwrap();
|
||||
|
||||
// then
|
||||
assert_eq!(ap.dapps_addresses(app.clone()).unwrap(), vec![1.into(), 2.into()]);
|
||||
@ -515,6 +532,7 @@ mod tests {
|
||||
// given
|
||||
let ap = AccountProvider::transient_provider();
|
||||
let address = ap.new_account("test").unwrap();
|
||||
ap.set_address_name(1.into(), "1".into());
|
||||
|
||||
// When returning nothing
|
||||
ap.set_new_dapps_whitelist(Some(vec![])).unwrap();
|
||||
@ -524,6 +542,10 @@ mod tests {
|
||||
ap.set_new_dapps_whitelist(None).unwrap();
|
||||
assert_eq!(ap.dapps_addresses("app1".into()).unwrap(), vec![address]);
|
||||
|
||||
// change to non-existent account
|
||||
ap.set_new_dapps_whitelist(Some(vec![2.into()])).unwrap();
|
||||
assert_eq!(ap.dapps_addresses("app1".into()).unwrap(), vec![]);
|
||||
|
||||
// change to a whitelist
|
||||
ap.set_new_dapps_whitelist(Some(vec![1.into()])).unwrap();
|
||||
assert_eq!(ap.dapps_addresses("app1".into()).unwrap(), vec![1.into()]);
|
||||
|
@ -19,7 +19,7 @@
|
||||
use std::{fs, fmt, hash, ops};
|
||||
use std::sync::atomic::{self, AtomicUsize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use ethstore::ethkey::Address;
|
||||
use ethjson::misc::{
|
||||
@ -37,9 +37,9 @@ pub struct AddressBook {
|
||||
|
||||
impl AddressBook {
|
||||
/// Creates new address book at given directory.
|
||||
pub fn new(path: String) -> Self {
|
||||
pub fn new(path: &Path) -> Self {
|
||||
let mut r = AddressBook {
|
||||
cache: DiskMap::new(path, "address_book.json".into())
|
||||
cache: DiskMap::new(path, "address_book.json")
|
||||
};
|
||||
r.cache.revert(AccountMeta::read);
|
||||
r
|
||||
@ -200,11 +200,11 @@ pub struct DappsSettingsStore {
|
||||
|
||||
impl DappsSettingsStore {
|
||||
/// Creates new store at given directory path.
|
||||
pub fn new(path: String) -> Self {
|
||||
pub fn new(path: &Path) -> Self {
|
||||
let mut r = DappsSettingsStore {
|
||||
settings: DiskMap::new(path.clone(), "dapps_accounts.json".into()),
|
||||
policy: DiskMap::new(path.clone(), "dapps_policy.json".into()),
|
||||
history: DiskMap::new(path.clone(), "dapps_history.json".into()),
|
||||
settings: DiskMap::new(path, "dapps_accounts.json".into()),
|
||||
policy: DiskMap::new(path, "dapps_policy.json".into()),
|
||||
history: DiskMap::new(path, "dapps_history.json".into()),
|
||||
time: TimeProvider::Clock,
|
||||
};
|
||||
r.settings.revert(JsonSettings::read);
|
||||
@ -297,9 +297,8 @@ impl<K: hash::Hash + Eq, V> ops::DerefMut for DiskMap<K, V> {
|
||||
}
|
||||
|
||||
impl<K: hash::Hash + Eq, V> DiskMap<K, V> {
|
||||
pub fn new(path: String, file_name: String) -> Self {
|
||||
trace!(target: "diskmap", "new({})", path);
|
||||
let mut path: PathBuf = path.into();
|
||||
pub fn new(path: &Path, file_name: &str) -> Self {
|
||||
let mut path = path.to_owned();
|
||||
path.push(file_name);
|
||||
trace!(target: "diskmap", "path={:?}", path);
|
||||
DiskMap {
|
||||
@ -310,7 +309,7 @@ impl<K: hash::Hash + Eq, V> DiskMap<K, V> {
|
||||
}
|
||||
|
||||
pub fn transient() -> Self {
|
||||
let mut map = DiskMap::new(Default::default(), "diskmap.json".into());
|
||||
let mut map = DiskMap::new(&PathBuf::new(), "diskmap.json".into());
|
||||
map.transient = true;
|
||||
map
|
||||
}
|
||||
@ -354,27 +353,25 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn should_save_and_reload_address_book() {
|
||||
let temp = RandomTempPath::create_dir();
|
||||
let path = temp.as_str().to_owned();
|
||||
let mut b = AddressBook::new(path.clone());
|
||||
let path = RandomTempPath::create_dir();
|
||||
let mut b = AddressBook::new(&path);
|
||||
b.set_name(1.into(), "One".to_owned());
|
||||
b.set_meta(1.into(), "{1:1}".to_owned());
|
||||
let b = AddressBook::new(path);
|
||||
let b = AddressBook::new(&path);
|
||||
assert_eq!(b.get(), hash_map![1.into() => AccountMeta{name: "One".to_owned(), meta: "{1:1}".to_owned(), uuid: None}]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_remove_address() {
|
||||
let temp = RandomTempPath::create_dir();
|
||||
let path = temp.as_str().to_owned();
|
||||
let mut b = AddressBook::new(path.clone());
|
||||
let path = RandomTempPath::create_dir();
|
||||
let mut b = AddressBook::new(&path);
|
||||
|
||||
b.set_name(1.into(), "One".to_owned());
|
||||
b.set_name(2.into(), "Two".to_owned());
|
||||
b.set_name(3.into(), "Three".to_owned());
|
||||
b.remove(2.into());
|
||||
|
||||
let b = AddressBook::new(path);
|
||||
let b = AddressBook::new(&path);
|
||||
assert_eq!(b.get(), hash_map![
|
||||
1.into() => AccountMeta{name: "One".to_owned(), meta: "{}".to_owned(), uuid: None},
|
||||
3.into() => AccountMeta{name: "Three".to_owned(), meta: "{}".to_owned(), uuid: None}
|
||||
@ -384,15 +381,14 @@ mod tests {
|
||||
#[test]
|
||||
fn should_save_and_reload_dapps_settings() {
|
||||
// given
|
||||
let temp = RandomTempPath::create_dir();
|
||||
let path = temp.as_str().to_owned();
|
||||
let mut b = DappsSettingsStore::new(path.clone());
|
||||
let path = RandomTempPath::create_dir();
|
||||
let mut b = DappsSettingsStore::new(&path);
|
||||
|
||||
// when
|
||||
b.set_accounts("dappOne".into(), vec![1.into(), 2.into()]);
|
||||
|
||||
// then
|
||||
let b = DappsSettingsStore::new(path);
|
||||
let b = DappsSettingsStore::new(&path);
|
||||
assert_eq!(b.settings(), hash_map![
|
||||
"dappOne".into() => DappsSettings {
|
||||
accounts: vec![1.into(), 2.into()],
|
||||
@ -422,9 +418,8 @@ mod tests {
|
||||
#[test]
|
||||
fn should_store_dapps_policy() {
|
||||
// given
|
||||
let temp = RandomTempPath::create_dir();
|
||||
let path = temp.as_str().to_owned();
|
||||
let mut store = DappsSettingsStore::new(path.clone());
|
||||
let path = RandomTempPath::create_dir();
|
||||
let mut store = DappsSettingsStore::new(&path);
|
||||
|
||||
// Test default policy
|
||||
assert_eq!(store.policy(), NewDappsPolicy::AllAccounts);
|
||||
@ -433,7 +428,7 @@ mod tests {
|
||||
store.set_policy(NewDappsPolicy::Whitelist(vec![1.into(), 2.into()]));
|
||||
|
||||
// then
|
||||
let store = DappsSettingsStore::new(path);
|
||||
let store = DappsSettingsStore::new(&path);
|
||||
assert_eq!(store.policy.clone(), hash_map![
|
||||
"default".into() => NewDappsPolicy::Whitelist(vec![1.into(), 2.into()])
|
||||
]);
|
||||
|
@ -24,6 +24,8 @@ pub struct BestBlock {
|
||||
pub hash: H256,
|
||||
/// Best block number.
|
||||
pub number: BlockNumber,
|
||||
/// Best block timestamp.
|
||||
pub timestamp: u64,
|
||||
/// Best block total difficulty.
|
||||
pub total_difficulty: U256,
|
||||
/// Best block uncompressed bytes
|
||||
|
@ -485,6 +485,7 @@ impl BlockChain {
|
||||
let best_block_number = bc.block_number(&best_block_hash).unwrap();
|
||||
let best_block_total_difficulty = bc.block_details(&best_block_hash).unwrap().total_difficulty;
|
||||
let best_block_rlp = bc.block(&best_block_hash).unwrap().into_inner();
|
||||
let best_block_timestamp = BlockView::new(&best_block_rlp).header().timestamp();
|
||||
|
||||
let raw_first = bc.db.get(db::COL_EXTRA, b"first").unwrap().map(|v| v.to_vec());
|
||||
let mut best_ancient = bc.db.get(db::COL_EXTRA, b"ancient").unwrap().map(|h| H256::from_slice(&h));
|
||||
@ -533,6 +534,7 @@ impl BlockChain {
|
||||
number: best_block_number,
|
||||
total_difficulty: best_block_total_difficulty,
|
||||
hash: best_block_hash,
|
||||
timestamp: best_block_timestamp,
|
||||
block: best_block_rlp,
|
||||
};
|
||||
|
||||
@ -585,6 +587,7 @@ impl BlockChain {
|
||||
number: extras.number - 1,
|
||||
total_difficulty: best_block_total_difficulty,
|
||||
hash: hash,
|
||||
timestamp: BlockView::new(&best_block_rlp).header().timestamp(),
|
||||
block: best_block_rlp,
|
||||
};
|
||||
// update parent extras
|
||||
@ -738,6 +741,7 @@ impl BlockChain {
|
||||
blocks_blooms: self.prepare_block_blooms_update(bytes, &info),
|
||||
transactions_addresses: self.prepare_transaction_addresses_update(bytes, &info),
|
||||
info: info,
|
||||
timestamp: header.timestamp(),
|
||||
block: bytes
|
||||
}, is_best);
|
||||
|
||||
@ -786,6 +790,7 @@ impl BlockChain {
|
||||
blocks_blooms: self.prepare_block_blooms_update(bytes, &info),
|
||||
transactions_addresses: self.prepare_transaction_addresses_update(bytes, &info),
|
||||
info: info,
|
||||
timestamp: header.timestamp(),
|
||||
block: bytes,
|
||||
}, is_best);
|
||||
true
|
||||
@ -850,6 +855,7 @@ impl BlockChain {
|
||||
blocks_blooms: self.prepare_block_blooms_update(bytes, &info),
|
||||
transactions_addresses: self.prepare_transaction_addresses_update(bytes, &info),
|
||||
info: info.clone(),
|
||||
timestamp: header.timestamp(),
|
||||
block: bytes,
|
||||
}, true);
|
||||
|
||||
@ -921,6 +927,7 @@ impl BlockChain {
|
||||
hash: update.info.hash,
|
||||
number: update.info.number,
|
||||
total_difficulty: update.info.total_difficulty,
|
||||
timestamp: update.timestamp,
|
||||
block: update.block.to_vec(),
|
||||
});
|
||||
},
|
||||
@ -1206,6 +1213,11 @@ impl BlockChain {
|
||||
self.best_block.read().number
|
||||
}
|
||||
|
||||
/// Get best block timestamp.
|
||||
pub fn best_block_timestamp(&self) -> u64 {
|
||||
self.best_block.read().timestamp
|
||||
}
|
||||
|
||||
/// Get best block total difficulty.
|
||||
pub fn best_block_total_difficulty(&self) -> U256 {
|
||||
self.best_block.read().total_difficulty
|
||||
@ -1293,6 +1305,7 @@ impl BlockChain {
|
||||
genesis_hash: self.genesis_hash(),
|
||||
best_block_hash: best_block.hash.clone(),
|
||||
best_block_number: best_block.number,
|
||||
best_block_timestamp: best_block.timestamp,
|
||||
first_block_hash: self.first_block(),
|
||||
first_block_number: From::from(self.first_block_number()),
|
||||
ancient_block_hash: best_ancient_block.as_ref().map(|b| b.hash.clone()),
|
||||
|
@ -9,6 +9,8 @@ use super::extras::{BlockDetails, BlockReceipts, TransactionAddress, LogGroupPos
|
||||
pub struct ExtrasUpdate<'a> {
|
||||
/// Block info.
|
||||
pub info: BlockInfo,
|
||||
/// Block timestamp.
|
||||
pub timestamp: u64,
|
||||
/// Current block uncompressed rlp bytes
|
||||
pub block: &'a [u8],
|
||||
/// Modified block hashes.
|
||||
|
@ -1406,7 +1406,11 @@ impl BlockChainClient for Client {
|
||||
}
|
||||
|
||||
fn ready_transactions(&self) -> Vec<PendingTransaction> {
|
||||
self.miner.ready_transactions(self.chain.read().best_block_number())
|
||||
let (number, timestamp) = {
|
||||
let chain = self.chain.read();
|
||||
(chain.best_block_number(), chain.best_block_timestamp())
|
||||
};
|
||||
self.miner.ready_transactions(number, timestamp)
|
||||
}
|
||||
|
||||
fn queue_consensus_message(&self, message: Bytes) {
|
||||
|
@ -552,7 +552,7 @@ impl BlockChainClient for TestBlockChainClient {
|
||||
let mut adding = false;
|
||||
|
||||
let mut blocks = Vec::new();
|
||||
for (_, hash) in numbers_read.iter().sort_by(|tuple1, tuple2| tuple1.0.cmp(tuple2.0)) {
|
||||
for (_, hash) in numbers_read.iter().sorted_by(|tuple1, tuple2| tuple1.0.cmp(tuple2.0)) {
|
||||
if hash == to {
|
||||
if adding {
|
||||
blocks.push(hash.clone());
|
||||
@ -669,12 +669,14 @@ impl BlockChainClient for TestBlockChainClient {
|
||||
}
|
||||
|
||||
fn chain_info(&self) -> BlockChainInfo {
|
||||
let number = self.blocks.read().len() as BlockNumber - 1;
|
||||
BlockChainInfo {
|
||||
total_difficulty: *self.difficulty.read(),
|
||||
pending_total_difficulty: *self.difficulty.read(),
|
||||
genesis_hash: self.genesis_hash.clone(),
|
||||
best_block_hash: self.last_hash.read().clone(),
|
||||
best_block_number: self.blocks.read().len() as BlockNumber - 1,
|
||||
best_block_number: number,
|
||||
best_block_timestamp: number,
|
||||
first_block_hash: self.first_block.read().as_ref().map(|x| x.0),
|
||||
first_block_number: self.first_block.read().as_ref().map(|x| x.1),
|
||||
ancient_block_hash: self.ancient_block.read().as_ref().map(|x| x.0),
|
||||
@ -709,7 +711,8 @@ impl BlockChainClient for TestBlockChainClient {
|
||||
}
|
||||
|
||||
fn ready_transactions(&self) -> Vec<PendingTransaction> {
|
||||
self.miner.ready_transactions(self.chain_info().best_block_number)
|
||||
let info = self.chain_info();
|
||||
self.miner.ready_transactions(info.best_block_number, info.best_block_timestamp)
|
||||
}
|
||||
|
||||
fn signing_network_id(&self) -> Option<u64> { None }
|
||||
|
@ -159,7 +159,7 @@ impl IoHandler<()> for TransitionHandler {
|
||||
fn initialize(&self, io: &IoContext<()>) {
|
||||
if let Some(engine) = self.engine.upgrade() {
|
||||
io.register_timer_once(ENGINE_TIMEOUT_TOKEN, engine.remaining_step_duration().as_millis())
|
||||
.unwrap_or_else(|e| warn!(target: "poa", "Failed to start consensus step timer: {}.", e))
|
||||
.unwrap_or_else(|e| warn!(target: "engine", "Failed to start consensus step timer: {}.", e))
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,7 +168,7 @@ impl IoHandler<()> for TransitionHandler {
|
||||
if let Some(engine) = self.engine.upgrade() {
|
||||
engine.step();
|
||||
io.register_timer_once(ENGINE_TIMEOUT_TOKEN, engine.remaining_step_duration().as_millis())
|
||||
.unwrap_or_else(|e| warn!(target: "poa", "Failed to restart consensus step timer: {}.", e))
|
||||
.unwrap_or_else(|e| warn!(target: "engine", "Failed to restart consensus step timer: {}.", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -234,14 +234,14 @@ impl Engine for AuthorityRound {
|
||||
let step = self.step.load(AtomicOrdering::SeqCst);
|
||||
if self.is_step_proposer(step, header.author()) {
|
||||
if let Ok(signature) = self.signer.sign(header.bare_hash()) {
|
||||
trace!(target: "poa", "generate_seal: Issuing a block for step {}.", step);
|
||||
trace!(target: "engine", "generate_seal: Issuing a block for step {}.", step);
|
||||
self.proposed.store(true, AtomicOrdering::SeqCst);
|
||||
return Seal::Regular(vec![encode(&step).to_vec(), encode(&(&H520::from(signature) as &[u8])).to_vec()]);
|
||||
} else {
|
||||
warn!(target: "poa", "generate_seal: FAIL: Accounts secret key unavailable.");
|
||||
warn!(target: "engine", "generate_seal: FAIL: Accounts secret key unavailable.");
|
||||
}
|
||||
} else {
|
||||
trace!(target: "poa", "generate_seal: Not a proposer for step {}.", step);
|
||||
trace!(target: "engine", "generate_seal: Not a proposer for step {}.", step);
|
||||
}
|
||||
Seal::None
|
||||
}
|
||||
@ -260,7 +260,7 @@ impl Engine for AuthorityRound {
|
||||
/// Check the number of seal fields.
|
||||
fn verify_block_basic(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
|
||||
if header.seal().len() != self.seal_fields() {
|
||||
trace!(target: "poa", "verify_block_basic: wrong number of seal fields");
|
||||
trace!(target: "engine", "verify_block_basic: wrong number of seal fields");
|
||||
Err(From::from(BlockError::InvalidSealArity(
|
||||
Mismatch { expected: self.seal_fields(), found: header.seal().len() }
|
||||
)))
|
||||
@ -279,11 +279,11 @@ impl Engine for AuthorityRound {
|
||||
if verify_address(&correct_proposer, &proposer_signature, &header.bare_hash())? {
|
||||
Ok(())
|
||||
} else {
|
||||
trace!(target: "poa", "verify_block_unordered: bad proposer for step: {}", header_step);
|
||||
trace!(target: "engine", "verify_block_unordered: bad proposer for step: {}", header_step);
|
||||
Err(EngineError::NotProposer(Mismatch { expected: correct_proposer, found: header.author().clone() }))?
|
||||
}
|
||||
} else {
|
||||
trace!(target: "poa", "verify_block_unordered: block from the future");
|
||||
trace!(target: "engine", "verify_block_unordered: block from the future");
|
||||
self.validators.report_benign(header.author());
|
||||
Err(BlockError::InvalidSeal)?
|
||||
}
|
||||
@ -297,7 +297,7 @@ impl Engine for AuthorityRound {
|
||||
let step = header_step(header)?;
|
||||
// Check if parent is from a previous step.
|
||||
if step == header_step(parent)? {
|
||||
trace!(target: "poa", "Multiple blocks proposed for step {}.", step);
|
||||
trace!(target: "engine", "Multiple blocks proposed for step {}.", step);
|
||||
self.validators.report_malicious(header.author());
|
||||
Err(EngineError::DoubleVote(header.author().clone()))?;
|
||||
}
|
||||
|
@ -159,13 +159,13 @@ impl Tendermint {
|
||||
let message = ConsensusMessage::new(signature, h, r, *s, block_hash);
|
||||
let validator = self.signer.address();
|
||||
self.votes.vote(message.clone(), &validator);
|
||||
debug!(target: "poa", "Generated {:?} as {}.", message, validator);
|
||||
debug!(target: "engine", "Generated {:?} as {}.", message, validator);
|
||||
self.handle_valid_message(&message);
|
||||
|
||||
Some(message_rlp)
|
||||
},
|
||||
Err(e) => {
|
||||
trace!(target: "poa", "Could not sign the message {}", e);
|
||||
trace!(target: "engine", "Could not sign the message {}", e);
|
||||
None
|
||||
},
|
||||
}
|
||||
@ -186,7 +186,7 @@ impl Tendermint {
|
||||
|
||||
fn to_next_height(&self, height: Height) {
|
||||
let new_height = height + 1;
|
||||
debug!(target: "poa", "Received a Commit, transitioning to height {}.", new_height);
|
||||
debug!(target: "engine", "Received a Commit, transitioning to height {}.", new_height);
|
||||
self.last_lock.store(0, AtomicOrdering::SeqCst);
|
||||
self.height.store(new_height, AtomicOrdering::SeqCst);
|
||||
self.view.store(0, AtomicOrdering::SeqCst);
|
||||
@ -196,7 +196,7 @@ impl Tendermint {
|
||||
/// Use via step_service to transition steps.
|
||||
fn to_step(&self, step: Step) {
|
||||
if let Err(io_err) = self.step_service.send_message(step) {
|
||||
warn!(target: "poa", "Could not proceed to step {}.", io_err)
|
||||
warn!(target: "engine", "Could not proceed to step {}.", io_err)
|
||||
}
|
||||
*self.step.write() = step;
|
||||
match step {
|
||||
@ -212,10 +212,10 @@ impl Tendermint {
|
||||
self.generate_and_broadcast_message(block_hash);
|
||||
},
|
||||
Step::Precommit => {
|
||||
trace!(target: "poa", "to_step: Precommit.");
|
||||
trace!(target: "engine", "to_step: Precommit.");
|
||||
let block_hash = match *self.lock_change.read() {
|
||||
Some(ref m) if self.is_view(m) && m.block_hash.is_some() => {
|
||||
trace!(target: "poa", "Setting last lock: {}", m.vote_step.view);
|
||||
trace!(target: "engine", "Setting last lock: {}", m.vote_step.view);
|
||||
self.last_lock.store(m.vote_step.view, AtomicOrdering::SeqCst);
|
||||
m.block_hash
|
||||
},
|
||||
@ -224,7 +224,7 @@ impl Tendermint {
|
||||
self.generate_and_broadcast_message(block_hash);
|
||||
},
|
||||
Step::Commit => {
|
||||
trace!(target: "poa", "to_step: Commit.");
|
||||
trace!(target: "engine", "to_step: Commit.");
|
||||
// Commit the block using a complete signature set.
|
||||
let view = self.view.load(AtomicOrdering::SeqCst);
|
||||
let height = self.height.load(AtomicOrdering::SeqCst);
|
||||
@ -234,7 +234,7 @@ impl Tendermint {
|
||||
let proposal_step = VoteStep::new(height, view, Step::Propose);
|
||||
let precommit_step = VoteStep::new(proposal_step.height, proposal_step.view, Step::Precommit);
|
||||
if let Some(seal) = self.votes.seal_signatures(proposal_step, precommit_step, &block_hash) {
|
||||
trace!(target: "poa", "Collected seal: {:?}", seal);
|
||||
trace!(target: "engine", "Collected seal: {:?}", seal);
|
||||
let seal = vec![
|
||||
::rlp::encode(&view).to_vec(),
|
||||
::rlp::encode(&seal.proposal).to_vec(),
|
||||
@ -243,7 +243,7 @@ impl Tendermint {
|
||||
self.submit_seal(block_hash, seal);
|
||||
self.to_next_height(height);
|
||||
} else {
|
||||
warn!(target: "poa", "Not enough votes found!");
|
||||
warn!(target: "engine", "Not enough votes found!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -262,7 +262,7 @@ impl Tendermint {
|
||||
/// Find the designated for the given view.
|
||||
fn view_proposer(&self, height: Height, view: View) -> Address {
|
||||
let proposer_nonce = height + view;
|
||||
trace!(target: "poa", "Proposer nonce: {}", proposer_nonce);
|
||||
trace!(target: "engine", "Proposer nonce: {}", proposer_nonce);
|
||||
self.validators.get(proposer_nonce)
|
||||
}
|
||||
|
||||
@ -291,7 +291,7 @@ impl Tendermint {
|
||||
}
|
||||
|
||||
fn increment_view(&self, n: View) {
|
||||
trace!(target: "poa", "increment_view: New view.");
|
||||
trace!(target: "engine", "increment_view: New view.");
|
||||
self.view.fetch_add(n, AtomicOrdering::SeqCst);
|
||||
}
|
||||
|
||||
@ -331,7 +331,7 @@ impl Tendermint {
|
||||
&& message.block_hash.is_some()
|
||||
&& self.has_enough_aligned_votes(message);
|
||||
if lock_change {
|
||||
trace!(target: "poa", "handle_valid_message: Lock change.");
|
||||
trace!(target: "engine", "handle_valid_message: Lock change.");
|
||||
*self.lock_change.write() = Some(message.clone());
|
||||
}
|
||||
// Check if it can affect the step transition.
|
||||
@ -349,7 +349,7 @@ impl Tendermint {
|
||||
self.increment_view(vote_step.view - self.view.load(AtomicOrdering::SeqCst));
|
||||
Some(Step::Precommit)
|
||||
},
|
||||
// Avoid counting twice.
|
||||
// Avoid counting votes twice.
|
||||
Step::Prevote if lock_change => Some(Step::Precommit),
|
||||
Step::Prevote if self.has_enough_aligned_votes(message) => Some(Step::Precommit),
|
||||
Step::Prevote if self.has_enough_future_step_votes(&vote_step) => {
|
||||
@ -360,7 +360,7 @@ impl Tendermint {
|
||||
};
|
||||
|
||||
if let Some(step) = next_step {
|
||||
trace!(target: "poa", "Transition to {:?} triggered.", step);
|
||||
trace!(target: "engine", "Transition to {:?} triggered.", step);
|
||||
self.to_step(step);
|
||||
}
|
||||
}
|
||||
@ -429,7 +429,7 @@ impl Engine for Tendermint {
|
||||
let vote_info = message_info_rlp(&VoteStep::new(height, view, Step::Propose), bh.clone());
|
||||
if let Ok(signature) = self.signer.sign(vote_info.sha3()).map(Into::into) {
|
||||
// Insert Propose vote.
|
||||
debug!(target: "poa", "Submitting proposal {} at height {} view {}.", header.bare_hash(), height, view);
|
||||
debug!(target: "engine", "Submitting proposal {} at height {} view {}.", header.bare_hash(), height, view);
|
||||
self.votes.vote(ConsensusMessage::new(signature, height, view, Step::Propose, bh), author);
|
||||
// Remember proposal for later seal submission.
|
||||
*self.proposal.write() = bh;
|
||||
@ -439,7 +439,7 @@ impl Engine for Tendermint {
|
||||
::rlp::EMPTY_LIST_RLP.to_vec()
|
||||
])
|
||||
} else {
|
||||
warn!(target: "poa", "generate_seal: FAIL: accounts secret key unavailable");
|
||||
warn!(target: "engine", "generate_seal: FAIL: accounts secret key unavailable");
|
||||
Seal::None
|
||||
}
|
||||
}
|
||||
@ -457,7 +457,7 @@ impl Engine for Tendermint {
|
||||
self.validators.report_malicious(&sender);
|
||||
Err(EngineError::DoubleVote(sender))?
|
||||
}
|
||||
trace!(target: "poa", "Handling a valid {:?} from {}.", message, sender);
|
||||
trace!(target: "engine", "Handling a valid {:?} from {}.", message, sender);
|
||||
self.handle_valid_message(&message);
|
||||
}
|
||||
Ok(())
|
||||
@ -519,7 +519,7 @@ impl Engine for Tendermint {
|
||||
if origins.insert(address) {
|
||||
signature_count += 1;
|
||||
} else {
|
||||
warn!(target: "poa", "verify_block_unordered: Duplicate signature from {} on the seal.", address);
|
||||
warn!(target: "engine", "verify_block_unordered: Duplicate signature from {} on the seal.", address);
|
||||
Err(BlockError::InvalidSeal)?;
|
||||
}
|
||||
}
|
||||
@ -577,12 +577,12 @@ impl Engine for Tendermint {
|
||||
let proposal = ConsensusMessage::new_proposal(header).expect("block went through full verification; this Engine verifies new_proposal creation; qed");
|
||||
if signatures_len != 1 {
|
||||
// New Commit received, skip to next height.
|
||||
trace!(target: "poa", "Received a commit: {:?}.", proposal.vote_step);
|
||||
trace!(target: "engine", "Received a commit: {:?}.", proposal.vote_step);
|
||||
self.to_next_height(proposal.vote_step.height);
|
||||
return false;
|
||||
}
|
||||
let proposer = proposal.verify().expect("block went through full verification; this Engine tries verify; qed");
|
||||
debug!(target: "poa", "Received a new proposal {:?} from {}.", proposal.vote_step, proposer);
|
||||
debug!(target: "engine", "Received a new proposal {:?} from {}.", proposal.vote_step, proposer);
|
||||
if self.is_view(&proposal) {
|
||||
*self.proposal.write() = proposal.block_hash.clone();
|
||||
}
|
||||
@ -594,7 +594,7 @@ impl Engine for Tendermint {
|
||||
fn step(&self) {
|
||||
let next_step = match *self.step.read() {
|
||||
Step::Propose => {
|
||||
trace!(target: "poa", "Propose timeout.");
|
||||
trace!(target: "engine", "Propose timeout.");
|
||||
if self.proposal.read().is_none() {
|
||||
// Report the proposer if no proposal was received.
|
||||
let current_proposer = self.view_proposer(self.height.load(AtomicOrdering::SeqCst), self.view.load(AtomicOrdering::SeqCst));
|
||||
@ -603,26 +603,26 @@ impl Engine for Tendermint {
|
||||
Step::Prevote
|
||||
},
|
||||
Step::Prevote if self.has_enough_any_votes() => {
|
||||
trace!(target: "poa", "Prevote timeout.");
|
||||
trace!(target: "engine", "Prevote timeout.");
|
||||
Step::Precommit
|
||||
},
|
||||
Step::Prevote => {
|
||||
trace!(target: "poa", "Prevote timeout without enough votes.");
|
||||
trace!(target: "engine", "Prevote timeout without enough votes.");
|
||||
self.broadcast_old_messages();
|
||||
Step::Prevote
|
||||
},
|
||||
Step::Precommit if self.has_enough_any_votes() => {
|
||||
trace!(target: "poa", "Precommit timeout.");
|
||||
trace!(target: "engine", "Precommit timeout.");
|
||||
self.increment_view(1);
|
||||
Step::Propose
|
||||
},
|
||||
Step::Precommit => {
|
||||
trace!(target: "poa", "Precommit timeout without enough votes.");
|
||||
trace!(target: "engine", "Precommit timeout without enough votes.");
|
||||
self.broadcast_old_messages();
|
||||
Step::Precommit
|
||||
},
|
||||
Step::Commit => {
|
||||
trace!(target: "poa", "Commit timeout.");
|
||||
trace!(target: "engine", "Commit timeout.");
|
||||
Step::Propose
|
||||
},
|
||||
};
|
||||
@ -838,7 +838,6 @@ mod tests {
|
||||
|
||||
let (b, seal) = propose_default(&spec, proposer);
|
||||
assert!(b.lock().try_seal(spec.engine.as_ref(), seal).is_ok());
|
||||
spec.engine.stop();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -850,7 +849,6 @@ mod tests {
|
||||
let (b, seal) = propose_default(&spec, proposer);
|
||||
let sealed = b.lock().seal(spec.engine.as_ref(), seal).unwrap();
|
||||
assert!(spec.engine.is_proposal(sealed.header()));
|
||||
spec.engine.stop();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -858,7 +856,7 @@ mod tests {
|
||||
let (spec, tap) = setup();
|
||||
let engine = spec.engine.clone();
|
||||
|
||||
let v0 = insert_and_register(&tap, engine.as_ref(), "0");
|
||||
let v0 = insert_and_unlock(&tap, "0");
|
||||
let v1 = insert_and_register(&tap, engine.as_ref(), "1");
|
||||
|
||||
let h = 1;
|
||||
@ -883,7 +881,6 @@ mod tests {
|
||||
assert!(notify.messages.read().contains(&prevote_current));
|
||||
assert!(notify.messages.read().contains(&precommit_current));
|
||||
assert!(notify.messages.read().contains(&prevote_future));
|
||||
engine.stop();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -933,7 +930,5 @@ mod tests {
|
||||
// Last precommit.
|
||||
vote(engine, |mh| tap.sign(v0, None, mh).map(H520::from), h, r, Step::Precommit, proposal);
|
||||
assert_eq!(client.chain_info().best_block_number, 1);
|
||||
|
||||
engine.stop();
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,9 @@ fn set_timeout<S: Sync + Send + Clone>(io: &IoContext<S>, timeout: Duration) {
|
||||
|
||||
impl <S> IoHandler<S> for TransitionHandler<S> where S: Sync + Send + Clone + 'static {
|
||||
fn initialize(&self, io: &IoContext<S>) {
|
||||
set_timeout(io, self.timeouts.initial());
|
||||
let initial = self.timeouts.initial();
|
||||
trace!(target: "engine", "Setting the initial timeout to {}.", initial);
|
||||
set_timeout(io, initial);
|
||||
}
|
||||
|
||||
/// Call step after timeout.
|
||||
|
@ -118,13 +118,13 @@ impl <M: Message + Default + Encodable + Debug> VoteCollector<M> {
|
||||
.get(&message.round())
|
||||
.map_or(false, |c| {
|
||||
let is_known = c.messages.contains(message);
|
||||
if is_known { trace!(target: "poa", "Known message: {:?}.", message); }
|
||||
if is_known { trace!(target: "engine", "Known message: {:?}.", message); }
|
||||
is_known
|
||||
})
|
||||
|| {
|
||||
let guard = self.votes.read();
|
||||
let is_old = guard.keys().next().map_or(true, |oldest| message.round() <= oldest);
|
||||
if is_old { trace!(target: "poa", "Old message {:?}.", message); }
|
||||
if is_old { trace!(target: "engine", "Old message {:?}.", message); }
|
||||
is_old
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ use client::TransactionImportResult;
|
||||
use executive::contract_address;
|
||||
use block::{ClosedBlock, IsBlock, Block};
|
||||
use error::*;
|
||||
use transaction::{Action, UnverifiedTransaction, PendingTransaction, SignedTransaction};
|
||||
use transaction::{Action, UnverifiedTransaction, PendingTransaction, SignedTransaction, Condition as TransactionCondition};
|
||||
use receipt::{Receipt, RichReceipt};
|
||||
use spec::Spec;
|
||||
use engines::{Engine, Seal};
|
||||
@ -325,7 +325,7 @@ impl Miner {
|
||||
let _timer = PerfTimer::new("prepare_block");
|
||||
let chain_info = chain.chain_info();
|
||||
let (transactions, mut open_block, original_work_hash) = {
|
||||
let transactions = {self.transaction_queue.lock().top_transactions_at(chain_info.best_block_number)};
|
||||
let transactions = {self.transaction_queue.lock().top_transactions_at(chain_info.best_block_number, chain_info.best_block_timestamp)};
|
||||
let mut sealing_work = self.sealing_work.lock();
|
||||
let last_work_hash = sealing_work.queue.peek_last_ref().map(|pb| pb.block().fields().header.hash());
|
||||
let best_hash = chain_info.best_block_hash;
|
||||
@ -597,7 +597,7 @@ impl Miner {
|
||||
client: &MiningBlockChainClient,
|
||||
transactions: Vec<UnverifiedTransaction>,
|
||||
default_origin: TransactionOrigin,
|
||||
min_block: Option<BlockNumber>,
|
||||
condition: Option<TransactionCondition>,
|
||||
transaction_queue: &mut BanningTransactionQueue,
|
||||
) -> Vec<Result<TransactionImportResult, Error>> {
|
||||
let accounts = self.accounts.as_ref()
|
||||
@ -635,7 +635,7 @@ impl Miner {
|
||||
let details_provider = TransactionDetailsProvider::new(client, &self.service_transaction_action);
|
||||
match origin {
|
||||
TransactionOrigin::Local | TransactionOrigin::RetractedBlock => {
|
||||
transaction_queue.add(transaction, origin, insertion_time, min_block, &details_provider)
|
||||
transaction_queue.add(transaction, origin, insertion_time, condition.clone(), &details_provider)
|
||||
},
|
||||
TransactionOrigin::External => {
|
||||
transaction_queue.add_with_banlist(transaction, insertion_time, &details_provider)
|
||||
@ -892,7 +892,7 @@ impl MinerService for Miner {
|
||||
let mut transaction_queue = self.transaction_queue.lock();
|
||||
// We need to re-validate transactions
|
||||
let import = self.add_transactions_to_queue(
|
||||
chain, vec![pending.transaction.into()], TransactionOrigin::Local, pending.min_block, &mut transaction_queue
|
||||
chain, vec![pending.transaction.into()], TransactionOrigin::Local, pending.condition, &mut transaction_queue
|
||||
).pop().expect("one result returned per added transaction; one added => one result; qed");
|
||||
|
||||
match import {
|
||||
@ -927,7 +927,7 @@ impl MinerService for Miner {
|
||||
|
||||
fn pending_transactions(&self) -> Vec<PendingTransaction> {
|
||||
let queue = self.transaction_queue.lock();
|
||||
queue.pending_transactions(BlockNumber::max_value())
|
||||
queue.pending_transactions(BlockNumber::max_value(), u64::max_value())
|
||||
}
|
||||
|
||||
fn local_transactions(&self) -> BTreeMap<H256, LocalTransactionStatus> {
|
||||
@ -942,14 +942,14 @@ impl MinerService for Miner {
|
||||
self.transaction_queue.lock().future_transactions()
|
||||
}
|
||||
|
||||
fn ready_transactions(&self, best_block: BlockNumber) -> Vec<PendingTransaction> {
|
||||
fn ready_transactions(&self, best_block: BlockNumber, best_block_timestamp: u64) -> Vec<PendingTransaction> {
|
||||
let queue = self.transaction_queue.lock();
|
||||
match self.options.pending_set {
|
||||
PendingSet::AlwaysQueue => queue.pending_transactions(best_block),
|
||||
PendingSet::AlwaysQueue => queue.pending_transactions(best_block, best_block_timestamp),
|
||||
PendingSet::SealingOrElseQueue => {
|
||||
self.from_pending_block(
|
||||
best_block,
|
||||
|| queue.pending_transactions(best_block),
|
||||
|| queue.pending_transactions(best_block, best_block_timestamp),
|
||||
|sealing| sealing.transactions().iter().map(|t| t.clone().into()).collect()
|
||||
)
|
||||
},
|
||||
@ -1325,7 +1325,7 @@ mod tests {
|
||||
// then
|
||||
assert_eq!(res.unwrap(), TransactionImportResult::Current);
|
||||
assert_eq!(miner.pending_transactions().len(), 1);
|
||||
assert_eq!(miner.ready_transactions(best_block).len(), 1);
|
||||
assert_eq!(miner.ready_transactions(best_block, 0).len(), 1);
|
||||
assert_eq!(miner.pending_transactions_hashes(best_block).len(), 1);
|
||||
assert_eq!(miner.pending_receipts(best_block).len(), 1);
|
||||
// This method will let us know if pending block was created (before calling that method)
|
||||
@ -1345,7 +1345,7 @@ mod tests {
|
||||
// then
|
||||
assert_eq!(res.unwrap(), TransactionImportResult::Current);
|
||||
assert_eq!(miner.pending_transactions().len(), 1);
|
||||
assert_eq!(miner.ready_transactions(best_block).len(), 0);
|
||||
assert_eq!(miner.ready_transactions(best_block, 0).len(), 0);
|
||||
assert_eq!(miner.pending_transactions_hashes(best_block).len(), 0);
|
||||
assert_eq!(miner.pending_receipts(best_block).len(), 0);
|
||||
}
|
||||
@ -1364,7 +1364,7 @@ mod tests {
|
||||
assert_eq!(res.unwrap(), TransactionImportResult::Current);
|
||||
assert_eq!(miner.pending_transactions().len(), 1);
|
||||
assert_eq!(miner.pending_transactions_hashes(best_block).len(), 0);
|
||||
assert_eq!(miner.ready_transactions(best_block).len(), 0);
|
||||
assert_eq!(miner.ready_transactions(best_block, 0).len(), 0);
|
||||
assert_eq!(miner.pending_receipts(best_block).len(), 0);
|
||||
// This method will let us know if pending block was created (before calling that method)
|
||||
assert!(miner.prepare_work_sealing(&client));
|
||||
|
@ -154,7 +154,7 @@ pub trait MinerService : Send + Sync {
|
||||
fn pending_transactions(&self) -> Vec<PendingTransaction>;
|
||||
|
||||
/// Get a list of all transactions that can go into the given block.
|
||||
fn ready_transactions(&self, best_block: BlockNumber) -> Vec<PendingTransaction>;
|
||||
fn ready_transactions(&self, best_block: BlockNumber, best_block_timestamp: u64) -> Vec<PendingTransaction>;
|
||||
|
||||
/// Get a list of all future transactions.
|
||||
fn future_transactions(&self) -> Vec<PendingTransaction>;
|
||||
|
@ -276,17 +276,17 @@ struct VerifiedTransaction {
|
||||
origin: TransactionOrigin,
|
||||
/// Insertion time
|
||||
insertion_time: QueuingInstant,
|
||||
/// Delay until specifid block.
|
||||
min_block: Option<BlockNumber>,
|
||||
/// Delay until specified condition is met.
|
||||
condition: Option<Condition>,
|
||||
}
|
||||
|
||||
impl VerifiedTransaction {
|
||||
fn new(transaction: SignedTransaction, origin: TransactionOrigin, time: QueuingInstant, min_block: Option<BlockNumber>) -> Self {
|
||||
fn new(transaction: SignedTransaction, origin: TransactionOrigin, time: QueuingInstant, condition: Option<Condition>) -> Self {
|
||||
VerifiedTransaction {
|
||||
transaction: transaction,
|
||||
origin: origin,
|
||||
insertion_time: time,
|
||||
min_block: min_block,
|
||||
condition: condition,
|
||||
}
|
||||
}
|
||||
|
||||
@ -666,14 +666,14 @@ impl TransactionQueue {
|
||||
tx: SignedTransaction,
|
||||
origin: TransactionOrigin,
|
||||
time: QueuingInstant,
|
||||
min_block: Option<BlockNumber>,
|
||||
condition: Option<Condition>,
|
||||
details_provider: &TransactionDetailsProvider,
|
||||
) -> Result<TransactionImportResult, Error> {
|
||||
if origin == TransactionOrigin::Local {
|
||||
let hash = tx.hash();
|
||||
let cloned_tx = tx.clone();
|
||||
|
||||
let result = self.add_internal(tx, origin, time, min_block, details_provider);
|
||||
let result = self.add_internal(tx, origin, time, condition, details_provider);
|
||||
match result {
|
||||
Ok(TransactionImportResult::Current) => {
|
||||
self.local_transactions.mark_pending(hash);
|
||||
@ -694,7 +694,7 @@ impl TransactionQueue {
|
||||
}
|
||||
result
|
||||
} else {
|
||||
self.add_internal(tx, origin, time, min_block, details_provider)
|
||||
self.add_internal(tx, origin, time, condition, details_provider)
|
||||
}
|
||||
}
|
||||
|
||||
@ -704,7 +704,7 @@ impl TransactionQueue {
|
||||
tx: SignedTransaction,
|
||||
origin: TransactionOrigin,
|
||||
time: QueuingInstant,
|
||||
min_block: Option<BlockNumber>,
|
||||
condition: Option<Condition>,
|
||||
details_provider: &TransactionDetailsProvider,
|
||||
) -> Result<TransactionImportResult, Error> {
|
||||
if origin != TransactionOrigin::Local && tx.gas_price < self.minimal_gas_price {
|
||||
@ -815,7 +815,7 @@ impl TransactionQueue {
|
||||
}
|
||||
tx.check_low_s()?;
|
||||
// No invalid transactions beyond this point.
|
||||
let vtx = VerifiedTransaction::new(tx, origin, time, min_block);
|
||||
let vtx = VerifiedTransaction::new(tx, origin, time, condition);
|
||||
let r = self.import_tx(vtx, client_account.nonce).map_err(Error::Transaction);
|
||||
assert_eq!(self.future.by_priority.len() + self.current.by_priority.len(), self.by_hash.len());
|
||||
r
|
||||
@ -1068,11 +1068,11 @@ impl TransactionQueue {
|
||||
|
||||
/// Returns top transactions from the queue ordered by priority.
|
||||
pub fn top_transactions(&self) -> Vec<SignedTransaction> {
|
||||
self.top_transactions_at(BlockNumber::max_value())
|
||||
self.top_transactions_at(BlockNumber::max_value(), u64::max_value())
|
||||
|
||||
}
|
||||
|
||||
fn filter_pending_transaction<F>(&self, best_block: BlockNumber, mut f: F)
|
||||
fn filter_pending_transaction<F>(&self, best_block: BlockNumber, best_timestamp: u64, mut f: F)
|
||||
where F: FnMut(&VerifiedTransaction) {
|
||||
|
||||
let mut delayed = HashSet::new();
|
||||
@ -1082,7 +1082,12 @@ impl TransactionQueue {
|
||||
if delayed.contains(&sender) {
|
||||
continue;
|
||||
}
|
||||
if tx.min_block.unwrap_or(0) > best_block {
|
||||
let delay = match tx.condition {
|
||||
Some(Condition::Number(n)) => n > best_block,
|
||||
Some(Condition::Timestamp(t)) => t > best_timestamp,
|
||||
None => false,
|
||||
};
|
||||
if delay {
|
||||
delayed.insert(sender);
|
||||
continue;
|
||||
}
|
||||
@ -1091,16 +1096,16 @@ impl TransactionQueue {
|
||||
}
|
||||
|
||||
/// Returns top transactions from the queue ordered by priority.
|
||||
pub fn top_transactions_at(&self, best_block: BlockNumber) -> Vec<SignedTransaction> {
|
||||
pub fn top_transactions_at(&self, best_block: BlockNumber, best_timestamp: u64) -> Vec<SignedTransaction> {
|
||||
let mut r = Vec::new();
|
||||
self.filter_pending_transaction(best_block, |tx| r.push(tx.transaction.clone()));
|
||||
self.filter_pending_transaction(best_block, best_timestamp, |tx| r.push(tx.transaction.clone()));
|
||||
r
|
||||
}
|
||||
|
||||
/// Return all ready transactions.
|
||||
pub fn pending_transactions(&self, best_block: BlockNumber) -> Vec<PendingTransaction> {
|
||||
pub fn pending_transactions(&self, best_block: BlockNumber, best_timestamp: u64) -> Vec<PendingTransaction> {
|
||||
let mut r = Vec::new();
|
||||
self.filter_pending_transaction(best_block, |tx| r.push(PendingTransaction::new(tx.transaction.clone(), tx.min_block)));
|
||||
self.filter_pending_transaction(best_block, best_timestamp, |tx| r.push(PendingTransaction::new(tx.transaction.clone(), tx.condition.clone())));
|
||||
r
|
||||
}
|
||||
|
||||
@ -1109,7 +1114,7 @@ impl TransactionQueue {
|
||||
self.future.by_priority
|
||||
.iter()
|
||||
.map(|t| self.by_hash.get(&t.hash).expect("All transactions in `current` and `future` are always included in `by_hash`"))
|
||||
.map(|t| PendingTransaction { transaction: t.transaction.clone(), min_block: t.min_block })
|
||||
.map(|t| PendingTransaction { transaction: t.transaction.clone(), condition: t.condition.clone() })
|
||||
.collect()
|
||||
}
|
||||
|
||||
@ -1382,7 +1387,7 @@ pub mod test {
|
||||
use super::{TransactionSet, TransactionOrder, VerifiedTransaction};
|
||||
use miner::local_transactions::LocalTransactionsList;
|
||||
use client::TransactionImportResult;
|
||||
use transaction::{SignedTransaction, Transaction, Action};
|
||||
use transaction::{SignedTransaction, Transaction, Action, Condition};
|
||||
|
||||
pub struct DummyTransactionDetailsProvider {
|
||||
account_details: AccountDetails,
|
||||
@ -2178,15 +2183,15 @@ pub mod test {
|
||||
let (tx, tx2) = new_tx_pair_default(1.into(), 0.into());
|
||||
|
||||
// when
|
||||
let res1 = txq.add(tx.clone(), TransactionOrigin::External, 0, Some(1), &default_tx_provider()).unwrap();
|
||||
let res1 = txq.add(tx.clone(), TransactionOrigin::External, 0, Some(Condition::Number(1)), &default_tx_provider()).unwrap();
|
||||
let res2 = txq.add(tx2.clone(), TransactionOrigin::External, 0, None, &default_tx_provider()).unwrap();
|
||||
|
||||
// then
|
||||
assert_eq!(res1, TransactionImportResult::Current);
|
||||
assert_eq!(res2, TransactionImportResult::Current);
|
||||
let top = txq.top_transactions_at(0);
|
||||
let top = txq.top_transactions_at(0, 0);
|
||||
assert_eq!(top.len(), 0);
|
||||
let top = txq.top_transactions_at(1);
|
||||
let top = txq.top_transactions_at(1, 0);
|
||||
assert_eq!(top.len(), 2);
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ use spec::Spec;
|
||||
use views::BlockView;
|
||||
use util::stats::Histogram;
|
||||
use ethkey::{KeyPair, Secret};
|
||||
use transaction::{PendingTransaction, Transaction, Action};
|
||||
use transaction::{PendingTransaction, Transaction, Action, Condition};
|
||||
use miner::MinerService;
|
||||
|
||||
#[test]
|
||||
@ -299,7 +299,7 @@ fn does_not_propagate_delayed_transactions() {
|
||||
action: Action::Call(Address::default()),
|
||||
value: 0.into(),
|
||||
data: Vec::new(),
|
||||
}.sign(secret, None), Some(2));
|
||||
}.sign(secret, None), Some(Condition::Number(2)));
|
||||
let tx1 = PendingTransaction::new(Transaction {
|
||||
nonce: 1.into(),
|
||||
gas_price: 0.into(),
|
||||
|
@ -34,6 +34,8 @@ pub struct BlockChainInfo {
|
||||
pub best_block_hash: H256,
|
||||
/// Best blockchain block number.
|
||||
pub best_block_number: BlockNumber,
|
||||
/// Best blockchain block timestamp.
|
||||
pub best_block_timestamp: u64,
|
||||
/// Best ancient block hash.
|
||||
pub ancient_block_hash: Option<H256>,
|
||||
/// Best ancient block number.
|
||||
|
@ -52,6 +52,16 @@ impl Decodable for Action {
|
||||
}
|
||||
}
|
||||
|
||||
/// Transaction activation condition.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "ipc", binary)]
|
||||
pub enum Condition {
|
||||
/// Valid at this block number or later.
|
||||
Number(BlockNumber),
|
||||
/// Valid at this unix time or later.
|
||||
Timestamp(u64),
|
||||
}
|
||||
|
||||
/// A set of information describing an externally-originating message call
|
||||
/// or contract creation operation.
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
@ -448,16 +458,16 @@ impl Deref for LocalizedTransaction {
|
||||
pub struct PendingTransaction {
|
||||
/// Signed transaction data.
|
||||
pub transaction: SignedTransaction,
|
||||
/// To be activated at this block. `None` for immediately.
|
||||
pub min_block: Option<BlockNumber>,
|
||||
/// To be activated at this condition. `None` for immediately.
|
||||
pub condition: Option<Condition>,
|
||||
}
|
||||
|
||||
impl PendingTransaction {
|
||||
/// Create a new pending transaction from signed transaction.
|
||||
pub fn new(signed: SignedTransaction, min_block: Option<BlockNumber>) -> Self {
|
||||
pub fn new(signed: SignedTransaction, condition: Option<Condition>) -> Self {
|
||||
PendingTransaction {
|
||||
transaction: signed,
|
||||
min_block: min_block,
|
||||
condition: condition,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -466,7 +476,7 @@ impl From<SignedTransaction> for PendingTransaction {
|
||||
fn from(t: SignedTransaction) -> Self {
|
||||
PendingTransaction {
|
||||
transaction: t,
|
||||
min_block: None,
|
||||
condition: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ tiny-keccak = "1.0"
|
||||
docopt = { version = "0.6", optional = true }
|
||||
time = "0.1.34"
|
||||
lazy_static = "0.2"
|
||||
itertools = "0.4"
|
||||
itertools = "0.5"
|
||||
parking_lot = "0.3"
|
||||
ethcrypto = { path = "../ethcrypto" }
|
||||
ethcore-util = { path = "../util" }
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::mem;
|
||||
use std::path::PathBuf;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
|
||||
use crypto::KEY_ITERATIONS;
|
||||
@ -164,8 +165,8 @@ impl SecretStore for EthStore {
|
||||
self.store.update(account_ref, old, safe_account)
|
||||
}
|
||||
|
||||
fn local_path(&self) -> String {
|
||||
self.store.dir.path().map(|p| p.to_string_lossy().into_owned()).unwrap_or_else(|| String::new())
|
||||
fn local_path(&self) -> PathBuf {
|
||||
self.store.dir.path().cloned().unwrap_or_else(PathBuf::new)
|
||||
}
|
||||
|
||||
fn list_geth_accounts(&self, testnet: bool) -> Vec<Address> {
|
||||
|
@ -15,6 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::PathBuf;
|
||||
use ethkey::{Address, Message, Signature, Secret, Public};
|
||||
use Error;
|
||||
use json::Uuid;
|
||||
@ -73,7 +74,7 @@ pub trait SecretStore: SimpleSecretStore {
|
||||
fn set_name(&self, account: &StoreAccountRef, name: String) -> Result<(), Error>;
|
||||
fn set_meta(&self, account: &StoreAccountRef, meta: String) -> Result<(), Error>;
|
||||
|
||||
fn local_path(&self) -> String;
|
||||
fn local_path(&self) -> PathBuf;
|
||||
fn list_geth_accounts(&self, testnet: bool) -> Vec<Address>;
|
||||
fn import_geth_accounts(&self, vault: SecretVaultRef, desired: Vec<Address>, testnet: bool) -> Result<Vec<StoreAccountRef>, Error>;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parity.js",
|
||||
"version": "0.3.58",
|
||||
"version": "0.3.62",
|
||||
"main": "release/index.js",
|
||||
"jsnext:main": "src/index.js",
|
||||
"author": "Parity Team <admin@parity.io>",
|
||||
|
@ -16,11 +16,12 @@
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { isPlainObject } from 'lodash';
|
||||
|
||||
import { info, warn, error } from './helpers/log';
|
||||
import { Dummy } from '../src/jsonrpc/helpers';
|
||||
import interfaces from '../src/jsonrpc';
|
||||
import rustMethods from './helpers/parsed-rpc-traits';
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, '../docs');
|
||||
|
||||
@ -28,20 +29,13 @@ if (!fs.existsSync(ROOT_DIR)) {
|
||||
fs.mkdirSync(ROOT_DIR);
|
||||
}
|
||||
|
||||
// INFO Logging helper
|
||||
function info (log) {
|
||||
console.log(chalk.blue(`INFO:\t${log}`));
|
||||
}
|
||||
|
||||
// WARN Logging helper
|
||||
function warn (log) {
|
||||
console.warn(chalk.yellow(`WARN:\t${log}`));
|
||||
}
|
||||
|
||||
// ERROR Logging helper
|
||||
function error (log) {
|
||||
console.error(chalk.red(`ERROR:\t${log}`));
|
||||
Object.keys(rustMethods).forEach((group) => {
|
||||
Object.keys(rustMethods[group]).forEach((method) => {
|
||||
if (interfaces[group] == null || interfaces[group][method] == null) {
|
||||
error(`${group}_${method} is defined in Rust traits, but not in js/src/jsonrpc/interfaces`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function printType (type) {
|
||||
return type.print || `\`${type.name}\``;
|
||||
@ -291,7 +285,8 @@ Object.keys(interfaces).sort().forEach((group) => {
|
||||
|
||||
Object.keys(spec).sort(methodComparator).forEach((iname) => {
|
||||
const method = spec[iname];
|
||||
const name = `${group.replace(/_.*$/, '')}_${iname}`;
|
||||
const groupName = group.replace(/_.*$/, '');
|
||||
const name = `${groupName}_${iname}`;
|
||||
|
||||
if (method.nodoc || method.deprecated) {
|
||||
info(`Skipping ${name}: ${method.nodoc || 'Deprecated'}`);
|
||||
@ -299,6 +294,10 @@ Object.keys(interfaces).sort().forEach((group) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rustMethods[groupName] == null || rustMethods[groupName][iname] == null) {
|
||||
error(`${name} is defined in js/src/jsonrpc/interfaces, but not in Rust traits`);
|
||||
}
|
||||
|
||||
const desc = method.desc;
|
||||
const params = buildParameters(method.params);
|
||||
const returns = `- ${formatType(method.returns)}`;
|
||||
|
32
js/scripts/helpers/log.js
Normal file
32
js/scripts/helpers/log.js
Normal file
@ -0,0 +1,32 @@
|
||||
// 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 chalk from 'chalk';
|
||||
|
||||
// INFO Logging helper
|
||||
export function info (log) {
|
||||
console.log(chalk.blue(`INFO:\t${log}`));
|
||||
}
|
||||
|
||||
// WARN Logging helper
|
||||
export function warn (log) {
|
||||
console.warn(chalk.yellow(`WARN:\t${log}`));
|
||||
}
|
||||
|
||||
// ERROR Logging helper
|
||||
export function error (log) {
|
||||
console.error(chalk.red(`ERROR:\t${log}`));
|
||||
}
|
81
js/scripts/helpers/parsed-rpc-traits.js
Normal file
81
js/scripts/helpers/parsed-rpc-traits.js
Normal file
@ -0,0 +1,81 @@
|
||||
// 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 fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// ```js
|
||||
// rustMethods['eth']['call'] === true
|
||||
// ```
|
||||
const rustMethods = {};
|
||||
|
||||
export default rustMethods;
|
||||
|
||||
// Get a list of JSON-RPC from Rust trait source code
|
||||
function parseMethodsFromRust (source) {
|
||||
// Matching the custom `rpc` attribute with it's doc comment
|
||||
const attributePattern = /((?:\s*\/\/\/.*$)*)\s*#\[rpc\(([^)]+)\)]/gm;
|
||||
const commentPattern = /\s*\/\/\/\s*/g;
|
||||
const separatorPattern = /\s*,\s*/g;
|
||||
const assignPattern = /([\S]+)\s*=\s*"([^"]*)"/;
|
||||
const ignorePattern = /@(ignore|deprecated|unimplemented|alias)\b/i;
|
||||
|
||||
const methods = [];
|
||||
|
||||
source.toString().replace(attributePattern, (match, comment, props) => {
|
||||
comment = comment.replace(commentPattern, '\n').trim();
|
||||
|
||||
// Skip deprecated methods
|
||||
if (ignorePattern.test(comment)) {
|
||||
return match;
|
||||
}
|
||||
|
||||
props.split(separatorPattern).forEach((prop) => {
|
||||
const [, key, value] = prop.split(assignPattern) || [];
|
||||
|
||||
if (key === 'name' && value != null) {
|
||||
methods.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
return methods;
|
||||
}
|
||||
|
||||
// Get a list of all JSON-RPC methods from all defined traits
|
||||
function getMethodsFromRustTraits () {
|
||||
const traitsDir = path.join(__dirname, '../../../rpc/src/v1/traits');
|
||||
|
||||
return fs.readdirSync(traitsDir)
|
||||
.filter((name) => name !== 'mod.rs' && /\.rs$/.test(name))
|
||||
.map((name) => fs.readFileSync(path.join(traitsDir, name)))
|
||||
.map(parseMethodsFromRust)
|
||||
.reduce((a, b) => a.concat(b));
|
||||
}
|
||||
|
||||
getMethodsFromRustTraits().sort().forEach((method) => {
|
||||
const [group, name] = method.split('_');
|
||||
|
||||
// Skip methods with malformed names
|
||||
if (group == null || name == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
rustMethods[group] = rustMethods[group] || {};
|
||||
rustMethods[group][name] = true;
|
||||
});
|
@ -23,6 +23,7 @@ export default class Eth {
|
||||
this._started = false;
|
||||
|
||||
this._lastBlock = new BigNumber(-1);
|
||||
this._pollTimerId = null;
|
||||
}
|
||||
|
||||
get isStarted () {
|
||||
@ -37,7 +38,7 @@ export default class Eth {
|
||||
|
||||
_blockNumber = () => {
|
||||
const nextTimeout = (timeout = 1000) => {
|
||||
setTimeout(() => {
|
||||
this._pollTimerId = setTimeout(() => {
|
||||
this._blockNumber();
|
||||
}, timeout);
|
||||
};
|
||||
@ -57,6 +58,6 @@ export default class Eth {
|
||||
|
||||
nextTimeout();
|
||||
})
|
||||
.catch(nextTimeout);
|
||||
.catch(() => nextTimeout());
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,9 @@ export default class Personal {
|
||||
this._api = api;
|
||||
this._updateSubscriptions = updateSubscriptions;
|
||||
this._started = false;
|
||||
|
||||
this._lastDefaultAccount = '0x0';
|
||||
this._pollTimerId = null;
|
||||
}
|
||||
|
||||
get isStarted () {
|
||||
@ -37,12 +40,35 @@ export default class Personal {
|
||||
]);
|
||||
}
|
||||
|
||||
_defaultAccount = () => {
|
||||
// FIXME: Because of the different API instances, the "wait for valid changes" approach
|
||||
// doesn't work. Since the defaultAccount is critical to operation, we poll in exactly
|
||||
// same way we do in ../eth (ala same as eth_blockNumber) and update. This should be moved
|
||||
// to pub-sub as it becomes available
|
||||
_defaultAccount = (timerDisabled = false) => {
|
||||
const nextTimeout = (timeout = 1000) => {
|
||||
if (!timerDisabled) {
|
||||
this._pollTimerId = setTimeout(() => {
|
||||
this._defaultAccount();
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
|
||||
if (!this._api.transport.isConnected) {
|
||||
nextTimeout(500);
|
||||
return;
|
||||
}
|
||||
|
||||
return this._api.parity
|
||||
.defaultAccount()
|
||||
.then((defaultAccount) => {
|
||||
if (this._lastDefaultAccount !== defaultAccount) {
|
||||
this._lastDefaultAccount = defaultAccount;
|
||||
this._updateSubscriptions('parity_defaultAccount', null, defaultAccount);
|
||||
});
|
||||
}
|
||||
|
||||
nextTimeout();
|
||||
})
|
||||
.catch(() => nextTimeout());
|
||||
}
|
||||
|
||||
_listAccounts = () => {
|
||||
@ -54,15 +80,21 @@ export default class Personal {
|
||||
}
|
||||
|
||||
_accountsInfo = () => {
|
||||
return Promise
|
||||
.all([
|
||||
this._api.parity.accountsInfo(),
|
||||
this._api.parity.allAccountsInfo()
|
||||
])
|
||||
.then(([info, allInfo]) => {
|
||||
return this._api.parity
|
||||
.accountsInfo()
|
||||
.then((info) => {
|
||||
this._updateSubscriptions('parity_accountsInfo', null, info);
|
||||
|
||||
return this._api.parity
|
||||
.allAccountsInfo()
|
||||
.catch(() => {
|
||||
// NOTE: This fails on non-secure APIs, swallow error
|
||||
return {};
|
||||
})
|
||||
.then((allInfo) => {
|
||||
this._updateSubscriptions('parity_allAccountsInfo', null, allInfo);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_loggingSubscribe () {
|
||||
@ -89,7 +121,7 @@ export default class Personal {
|
||||
|
||||
case 'parity_setDappsAddresses':
|
||||
case 'parity_setNewDappsWhitelist':
|
||||
this._defaultAccount();
|
||||
this._defaultAccount(true);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
@ -36,6 +36,9 @@ function stubApi (accounts, info) {
|
||||
|
||||
return {
|
||||
_calls,
|
||||
transport: {
|
||||
isConnected: true
|
||||
},
|
||||
parity: {
|
||||
accountsInfo: () => {
|
||||
const stub = sinon.stub().resolves(info || TEST_INFO)();
|
||||
|
@ -20,23 +20,23 @@ export const checkIfVerified = (contract, account) => {
|
||||
return contract.instance.certified.call({}, [account]);
|
||||
};
|
||||
|
||||
export const checkIfRequested = (contract, account) => {
|
||||
export const findLastRequested = (contract, account) => {
|
||||
let subId = null;
|
||||
let resolved = false;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
contract
|
||||
.subscribe('Requested', {
|
||||
fromBlock: 0, toBlock: 'pending'
|
||||
fromBlock: 0,
|
||||
toBlock: 'pending',
|
||||
limit: 1,
|
||||
topics: [account]
|
||||
}, (err, logs) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
const e = logs.find((l) => {
|
||||
return l.type === 'mined' && l.params.who && l.params.who.value === account;
|
||||
});
|
||||
|
||||
resolve(e ? e.transactionHash : false);
|
||||
resolve(logs[0] || null);
|
||||
resolved = true;
|
||||
|
||||
if (subId) {
|
||||
|
@ -45,7 +45,7 @@ export default class Application extends Component {
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.attachInstance();
|
||||
return this.attachInstance();
|
||||
}
|
||||
|
||||
render () {
|
||||
@ -80,12 +80,12 @@ export default class Application extends Component {
|
||||
}
|
||||
|
||||
attachInstance () {
|
||||
Promise
|
||||
return Promise
|
||||
.all([
|
||||
attachInstances(),
|
||||
api.parity.accountsInfo()
|
||||
api.parity.accountsInfo(),
|
||||
attachInstances()
|
||||
])
|
||||
.then(([{ managerInstance, registryInstance, tokenregInstance }, accountsInfo]) => {
|
||||
.then(([accountsInfo, { managerInstance, registryInstance, tokenregInstance }]) => {
|
||||
accountsInfo = accountsInfo || {};
|
||||
this.setState({
|
||||
loading: false,
|
||||
|
@ -17,7 +17,6 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import { api } from '../../parity';
|
||||
import AddressSelect from '../../AddressSelect';
|
||||
import Container from '../../Container';
|
||||
import styles from './deployment.css';
|
||||
|
||||
@ -122,36 +121,13 @@ export default class Deployment extends Component {
|
||||
}
|
||||
|
||||
renderForm () {
|
||||
const { accounts } = this.context;
|
||||
const { baseText, name, nameError, tla, tlaError, totalSupply, totalSupplyError } = this.state;
|
||||
const hasError = !!(nameError || tlaError || totalSupplyError);
|
||||
const error = `${styles.input} ${styles.error}`;
|
||||
const addresses = Object.keys(accounts);
|
||||
|
||||
// <div className={ styles.input }>
|
||||
// <label>global registration</label>
|
||||
// <select onChange={ this.onChangeRegistrar }>
|
||||
// <option value='no'>No, only for me</option>
|
||||
// <option value='yes'>Yes, for everybody</option>
|
||||
// </select>
|
||||
// <div className={ styles.hint }>
|
||||
// register on network (fee: { globalFeeText }ETH)
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className={ styles.form }>
|
||||
<div className={ styles.input }>
|
||||
<label>deployment account</label>
|
||||
<AddressSelect
|
||||
addresses={ addresses }
|
||||
onChange={ this.onChangeFrom }
|
||||
/>
|
||||
<div className={ styles.hint }>
|
||||
the owner account to deploy from
|
||||
</div>
|
||||
</div>
|
||||
<div className={ nameError ? error : styles.input }>
|
||||
<label>token name</label>
|
||||
<input
|
||||
@ -206,12 +182,6 @@ export default class Deployment extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
onChangeFrom = (event) => {
|
||||
const fromAddress = event.target.value;
|
||||
|
||||
this.setState({ fromAddress });
|
||||
}
|
||||
|
||||
onChangeName = (event) => {
|
||||
const name = event.target.value;
|
||||
const nameError = name && (name.length > 2) && (name.length < 32)
|
||||
@ -271,7 +241,7 @@ export default class Deployment extends Component {
|
||||
|
||||
onDeploy = () => {
|
||||
const { managerInstance, registryInstance, tokenregInstance } = this.context;
|
||||
const { base, deployBusy, fromAddress, globalReg, globalFee, name, nameError, tla, tlaError, totalSupply, totalSupplyError } = this.state;
|
||||
const { base, deployBusy, globalReg, globalFee, name, nameError, tla, tlaError, totalSupply, totalSupplyError } = this.state;
|
||||
const hasError = !!(nameError || tlaError || totalSupplyError);
|
||||
|
||||
if (hasError || deployBusy) {
|
||||
@ -281,14 +251,18 @@ export default class Deployment extends Component {
|
||||
const tokenreg = (globalReg ? tokenregInstance : registryInstance).address;
|
||||
const values = [base.mul(totalSupply), tla, name, tokenreg];
|
||||
const options = {
|
||||
from: fromAddress,
|
||||
value: globalReg ? globalFee : 0
|
||||
};
|
||||
|
||||
this.setState({ deployBusy: true, deployState: 'Estimating gas for the transaction' });
|
||||
|
||||
managerInstance
|
||||
.deploy.estimateGas(options, values)
|
||||
return api.parity
|
||||
.defaultAccount()
|
||||
.then((defaultAddress) => {
|
||||
options.from = defaultAddress;
|
||||
|
||||
return managerInstance.deploy.estimateGas(options, values);
|
||||
})
|
||||
.then((gas) => {
|
||||
this.setState({ deployState: 'Gas estimated, Posting transaction to the network' });
|
||||
|
||||
|
@ -25,6 +25,8 @@ let registryInstance;
|
||||
|
||||
const registries = {};
|
||||
const subscriptions = {};
|
||||
|
||||
let defaultSubscriptionId;
|
||||
let nextSubscriptionId = 1000;
|
||||
let isTest = false;
|
||||
|
||||
@ -65,6 +67,20 @@ export function unsubscribeEvents (subscriptionId) {
|
||||
delete subscriptions[subscriptionId];
|
||||
}
|
||||
|
||||
export function subscribeDefaultAddress (callback) {
|
||||
return api
|
||||
.subscribe('parity_defaultAccount', callback)
|
||||
.then((subscriptionId) => {
|
||||
defaultSubscriptionId = subscriptionId;
|
||||
|
||||
return defaultSubscriptionId;
|
||||
});
|
||||
}
|
||||
|
||||
export function unsubscribeDefaultAddress () {
|
||||
return api.unsubscribe(defaultSubscriptionId);
|
||||
}
|
||||
|
||||
function pollEvents () {
|
||||
const loop = Object.values(subscriptions);
|
||||
const timeout = () => setTimeout(pollEvents, 1000);
|
||||
|
@ -17,10 +17,9 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { api } from '../parity';
|
||||
import { attachInterface } from '../services';
|
||||
import { attachInterface, subscribeDefaultAddress, unsubscribeDefaultAddress } from '../services';
|
||||
import Button from '../Button';
|
||||
import Events from '../Events';
|
||||
import IdentityIcon from '../IdentityIcon';
|
||||
import Loading from '../Loading';
|
||||
|
||||
import styles from './application.css';
|
||||
@ -32,7 +31,7 @@ let nextEventId = 0;
|
||||
|
||||
export default class Application extends Component {
|
||||
state = {
|
||||
fromAddress: null,
|
||||
defaultAddress: null,
|
||||
loading: true,
|
||||
url: '',
|
||||
urlError: null,
|
||||
@ -47,19 +46,32 @@ export default class Application extends Component {
|
||||
registerType: 'file',
|
||||
repo: '',
|
||||
repoError: null,
|
||||
subscriptionId: null,
|
||||
events: {},
|
||||
eventIds: []
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
attachInterface()
|
||||
.then((state) => {
|
||||
this.setState(state, () => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
return Promise
|
||||
.all([
|
||||
attachInterface(),
|
||||
subscribeDefaultAddress((error, defaultAddress) => {
|
||||
if (!error) {
|
||||
this.setState({ defaultAddress });
|
||||
}
|
||||
})
|
||||
])
|
||||
.then(([state]) => {
|
||||
this.setState(Object.assign({}, state, {
|
||||
loading: false
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
return unsubscribeDefaultAddress();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { loading } = this.state;
|
||||
|
||||
@ -75,12 +87,14 @@ export default class Application extends Component {
|
||||
}
|
||||
|
||||
renderPage () {
|
||||
const { fromAddress, registerBusy, url, urlError, contentHash, contentHashError, contentHashOwner, commit, commitError, registerType, repo, repoError } = this.state;
|
||||
const { defaultAddress, registerBusy, url, urlError, contentHash, contentHashError, contentHashOwner, commit, commitError, registerType, repo, repoError } = this.state;
|
||||
|
||||
let hashClass = null;
|
||||
|
||||
if (contentHashError) {
|
||||
hashClass = contentHashOwner !== fromAddress ? styles.hashError : styles.hashWarning;
|
||||
hashClass = contentHashOwner !== defaultAddress
|
||||
? styles.hashError
|
||||
: styles.hashWarning;
|
||||
} else if (contentHash) {
|
||||
hashClass = styles.hashOk;
|
||||
}
|
||||
@ -166,20 +180,13 @@ export default class Application extends Component {
|
||||
}
|
||||
|
||||
renderButtons () {
|
||||
const { accounts, fromAddress, urlError, repoError, commitError, contentHashError, contentHashOwner } = this.state;
|
||||
const account = accounts[fromAddress];
|
||||
const { defaultAddress, urlError, repoError, commitError, contentHashError, contentHashOwner } = this.state;
|
||||
|
||||
return (
|
||||
<div className={ styles.buttons }>
|
||||
<div className={ styles.addressSelect }>
|
||||
<Button invert onClick={ this.onSelectFromAddress }>
|
||||
<IdentityIcon address={ account.address } />
|
||||
<div>{ account.name || account.address }</div>
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={ this.onClickRegister }
|
||||
disabled={ (contentHashError && contentHashOwner !== fromAddress) || urlError || repoError || commitError }
|
||||
disabled={ (contentHashError && contentHashOwner !== defaultAddress) || urlError || repoError || commitError }
|
||||
>register url</Button>
|
||||
</div>
|
||||
);
|
||||
@ -294,11 +301,11 @@ export default class Application extends Component {
|
||||
}
|
||||
|
||||
onClickRegister = () => {
|
||||
const { commit, commitError, contentHashError, contentHashOwner, fromAddress, url, urlError, registerType, repo, repoError } = this.state;
|
||||
const { defaultAddress, commit, commitError, contentHashError, contentHashOwner, url, urlError, registerType, repo, repoError } = this.state;
|
||||
|
||||
// TODO: No errors are currently set, validation to be expanded and added for each
|
||||
// field (query is fast to pick up the issues, so not burning atm)
|
||||
if ((contentHashError && contentHashOwner !== fromAddress) || repoError || urlError || commitError) {
|
||||
if ((contentHashError && contentHashOwner !== defaultAddress) || repoError || urlError || commitError) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -368,13 +375,15 @@ export default class Application extends Component {
|
||||
}
|
||||
|
||||
registerContent (contentRepo, contentCommit) {
|
||||
const { contentHash, fromAddress, instance } = this.state;
|
||||
const { defaultAddress, contentHash, instance } = this.state;
|
||||
|
||||
contentCommit = contentCommit.substr(0, 2) === '0x' ? contentCommit : `0x${contentCommit}`;
|
||||
contentCommit = contentCommit.substr(0, 2) === '0x'
|
||||
? contentCommit
|
||||
: `0x${contentCommit}`;
|
||||
|
||||
const eventId = nextEventId++;
|
||||
const values = [contentHash, contentRepo, contentCommit];
|
||||
const options = { from: fromAddress };
|
||||
const options = { from: defaultAddress };
|
||||
|
||||
this.setState({
|
||||
eventIds: [eventId].concat(this.state.eventIds),
|
||||
@ -383,7 +392,7 @@ export default class Application extends Component {
|
||||
contentHash,
|
||||
contentRepo,
|
||||
contentCommit,
|
||||
fromAddress,
|
||||
defaultAddress,
|
||||
registerBusy: true,
|
||||
registerState: 'Estimating gas for the transaction',
|
||||
timestamp: new Date()
|
||||
@ -421,11 +430,11 @@ export default class Application extends Component {
|
||||
}
|
||||
|
||||
registerUrl (contentUrl) {
|
||||
const { contentHash, fromAddress, instance } = this.state;
|
||||
const { contentHash, defaultAddress, instance } = this.state;
|
||||
|
||||
const eventId = nextEventId++;
|
||||
const values = [contentHash, contentUrl];
|
||||
const options = { from: fromAddress };
|
||||
const options = { from: defaultAddress };
|
||||
|
||||
this.setState({
|
||||
eventIds: [eventId].concat(this.state.eventIds),
|
||||
@ -433,7 +442,7 @@ export default class Application extends Component {
|
||||
[eventId]: {
|
||||
contentHash,
|
||||
contentUrl,
|
||||
fromAddress,
|
||||
defaultAddress,
|
||||
registerBusy: true,
|
||||
registerState: 'Estimating gas for the transaction',
|
||||
timestamp: new Date()
|
||||
@ -470,25 +479,6 @@ export default class Application extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
onSelectFromAddress = () => {
|
||||
const { accounts, fromAddress } = this.state;
|
||||
const addresses = Object.keys(accounts);
|
||||
let index = 0;
|
||||
|
||||
addresses.forEach((address, _index) => {
|
||||
if (address === fromAddress) {
|
||||
index = _index;
|
||||
}
|
||||
});
|
||||
|
||||
index++;
|
||||
if (index >= addresses.length) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
this.setState({ fromAddress: addresses[index] });
|
||||
}
|
||||
|
||||
lookupHash (url) {
|
||||
const { instance } = this.state;
|
||||
|
||||
|
@ -17,48 +17,44 @@
|
||||
import * as abis from '~/contracts/abi';
|
||||
import { api } from './parity';
|
||||
|
||||
let defaultSubscriptionId;
|
||||
|
||||
export function attachInterface () {
|
||||
return api.parity
|
||||
.registryAddress()
|
||||
.then((registryAddress) => {
|
||||
console.log(`the registry was found at ${registryAddress}`);
|
||||
|
||||
const registry = api.newContract(abis.registry, registryAddress).instance;
|
||||
|
||||
return Promise
|
||||
.all([
|
||||
registry.getAddress.call({}, [api.util.sha3('githubhint'), 'A']),
|
||||
api.parity.accountsInfo()
|
||||
]);
|
||||
return api
|
||||
.newContract(abis.registry, registryAddress).instance
|
||||
.getAddress.call({}, [api.util.sha3('githubhint'), 'A']);
|
||||
})
|
||||
.then(([address, accountsInfo]) => {
|
||||
.then((address) => {
|
||||
console.log(`githubhint was found at ${address}`);
|
||||
|
||||
const contract = api.newContract(abis.githubhint, address);
|
||||
const accounts = Object
|
||||
.keys(accountsInfo)
|
||||
.reduce((obj, address) => {
|
||||
const account = accountsInfo[address];
|
||||
|
||||
return Object.assign(obj, {
|
||||
[address]: {
|
||||
address,
|
||||
name: account.name
|
||||
}
|
||||
});
|
||||
}, {});
|
||||
const fromAddress = Object.keys(accounts)[0];
|
||||
|
||||
return {
|
||||
accounts,
|
||||
address,
|
||||
accountsInfo,
|
||||
contract,
|
||||
instance: contract.instance,
|
||||
fromAddress
|
||||
instance: contract.instance
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('attachInterface', error);
|
||||
});
|
||||
}
|
||||
|
||||
export function subscribeDefaultAddress (callback) {
|
||||
return api
|
||||
.subscribe('parity_defaultAccount', callback)
|
||||
.then((subscriptionId) => {
|
||||
defaultSubscriptionId = subscriptionId;
|
||||
|
||||
return defaultSubscriptionId;
|
||||
});
|
||||
}
|
||||
|
||||
export function unsubscribeDefaultAddress () {
|
||||
return api.unsubscribe(defaultSubscriptionId);
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ import { api } from '../parity';
|
||||
|
||||
import styles from './transaction.css';
|
||||
|
||||
import IdentityIcon from '../../githubhint/IdentityIcon';
|
||||
import IdentityIcon from '../IdentityIcon';
|
||||
|
||||
class BaseTransaction extends Component {
|
||||
shortHash (hash) {
|
||||
|
@ -82,6 +82,7 @@ export const ownerLookup = (name) => (dispatch, getState) => {
|
||||
return;
|
||||
}
|
||||
|
||||
name = name.toLowerCase();
|
||||
dispatch(ownerLookupStart(name));
|
||||
|
||||
return getOwner(contract, name)
|
||||
|
@ -30,7 +30,6 @@ export default class Application extends Component {
|
||||
state = {
|
||||
accounts: {},
|
||||
address: null,
|
||||
fromAddress: null,
|
||||
accountsInfo: {},
|
||||
blockNumber: new BigNumber(0),
|
||||
contract: null,
|
||||
@ -41,11 +40,9 @@ export default class Application extends Component {
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
attachInterface()
|
||||
return attachInterface()
|
||||
.then((state) => {
|
||||
this.setState(state, () => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
this.setState(Object.assign({}, state, { loading: false }));
|
||||
|
||||
return attachBlockNumber(state.instance, (state) => {
|
||||
this.setState(state);
|
||||
@ -86,17 +83,14 @@ export default class Application extends Component {
|
||||
}
|
||||
|
||||
renderImport () {
|
||||
const { accounts, fromAddress, instance, showImport } = this.state;
|
||||
const { instance, showImport } = this.state;
|
||||
|
||||
if (showImport) {
|
||||
return (
|
||||
<Import
|
||||
accounts={ accounts }
|
||||
fromAddress={ fromAddress }
|
||||
instance={ instance }
|
||||
visible={ showImport }
|
||||
onClose={ this.toggleImport }
|
||||
onSetFromAddress={ this.setFromAddress }
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -124,10 +118,4 @@ export default class Application extends Component {
|
||||
showImport: !this.state.showImport
|
||||
});
|
||||
}
|
||||
|
||||
setFromAddress = (fromAddress) => {
|
||||
this.setState({
|
||||
fromAddress
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -19,18 +19,14 @@ import React, { Component, PropTypes } from 'react';
|
||||
import { api } from '../parity';
|
||||
import { callRegister, postRegister } from '../services';
|
||||
import Button from '../Button';
|
||||
import IdentityIcon from '../IdentityIcon';
|
||||
|
||||
import styles from './import.css';
|
||||
|
||||
export default class Import extends Component {
|
||||
static propTypes = {
|
||||
accounts: PropTypes.object.isRequired,
|
||||
fromAddress: PropTypes.string.isRequired,
|
||||
instance: PropTypes.object.isRequired,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onSetFromAddress: PropTypes.func.isRequired
|
||||
onClose: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
state = {
|
||||
@ -83,21 +79,12 @@ export default class Import extends Component {
|
||||
}
|
||||
|
||||
renderRegister () {
|
||||
const { accounts, fromAddress } = this.props;
|
||||
|
||||
const account = accounts[fromAddress];
|
||||
const count = this.countFunctions();
|
||||
let buttons = null;
|
||||
|
||||
if (count) {
|
||||
buttons = (
|
||||
<div className={ styles.buttonrow }>
|
||||
<div className={ styles.addressSelect }>
|
||||
<Button invert onClick={ this.onSelectFromAddress }>
|
||||
<IdentityIcon address={ account.address } />
|
||||
<div>{ account.name || account.address }</div>
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={ this.onRegister }>
|
||||
register functions
|
||||
</Button>
|
||||
@ -197,15 +184,15 @@ export default class Import extends Component {
|
||||
}
|
||||
|
||||
onRegister = () => {
|
||||
const { instance, fromAddress, onClose } = this.props;
|
||||
const { instance, onClose } = this.props;
|
||||
const { functions, fnstate } = this.state;
|
||||
|
||||
Promise
|
||||
return Promise
|
||||
.all(
|
||||
functions
|
||||
.filter((fn) => !fn.constant)
|
||||
.filter((fn) => fnstate[fn.signature] === 'fntodo')
|
||||
.map((fn) => postRegister(instance, fn.id, { from: fromAddress }))
|
||||
.map((fn) => postRegister(instance, fn.id, {}))
|
||||
)
|
||||
.then(() => {
|
||||
onClose();
|
||||
@ -214,23 +201,4 @@ export default class Import extends Component {
|
||||
console.error('onRegister', error);
|
||||
});
|
||||
}
|
||||
|
||||
onSelectFromAddress = () => {
|
||||
const { accounts, fromAddress, onSetFromAddress } = this.props;
|
||||
const addresses = Object.keys(accounts);
|
||||
let index = 0;
|
||||
|
||||
addresses.forEach((address, _index) => {
|
||||
if (address === fromAddress) {
|
||||
index = _index;
|
||||
}
|
||||
});
|
||||
|
||||
index++;
|
||||
if (index >= addresses.length) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
onSetFromAddress(addresses[index]);
|
||||
}
|
||||
}
|
||||
|
@ -166,8 +166,13 @@ export function callRegister (instance, id, options = {}) {
|
||||
}
|
||||
|
||||
export function postRegister (instance, id, options = {}) {
|
||||
return instance.register
|
||||
.estimateGas(options, [id])
|
||||
return api.parity
|
||||
.defaultAccount()
|
||||
.then((defaultAddress) => {
|
||||
options.from = defaultAddress;
|
||||
|
||||
return instance.register.estimateGas(options, [id]);
|
||||
})
|
||||
.then((gas) => {
|
||||
options.gas = gas.mul(1.2).toFixed(0);
|
||||
console.log('postRegister', `gas estimated at ${gas.toFormat(0)}, setting to ${gas.mul(1.2).toFormat(0)}`);
|
||||
|
@ -14,8 +14,6 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import interfaces from './';
|
||||
import * as customTypes from './types';
|
||||
|
||||
@ -27,91 +25,13 @@ function verifyType (obj) {
|
||||
}
|
||||
}
|
||||
|
||||
// Get a list of JSON-RPC from Rust trait source code
|
||||
function parseMethodsFromRust (source) {
|
||||
// Matching the custom `rpc` attribute with it's doc comment
|
||||
const attributePattern = /((?:\s*\/\/\/.*$)*)\s*#\[rpc\(([^)]+)\)]/gm;
|
||||
const commentPattern = /\s*\/\/\/\s*/g;
|
||||
const separatorPattern = /\s*,\s*/g;
|
||||
const assignPattern = /([\S]+)\s*=\s*"([^"]*)"/;
|
||||
const ignorePattern = /@(ignore|deprecated|unimplemented|alias)\b/i;
|
||||
|
||||
const methods = [];
|
||||
|
||||
source.toString().replace(attributePattern, (match, comment, props) => {
|
||||
comment = comment.replace(commentPattern, '\n').trim();
|
||||
|
||||
// Skip deprecated methods
|
||||
if (ignorePattern.test(comment)) {
|
||||
return match;
|
||||
}
|
||||
|
||||
props.split(separatorPattern).forEach((prop) => {
|
||||
const [, key, value] = prop.split(assignPattern) || [];
|
||||
|
||||
if (key === 'name' && value != null) {
|
||||
methods.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
return methods;
|
||||
}
|
||||
|
||||
// Get a list of all JSON-RPC methods from all defined traits
|
||||
function getMethodsFromRustTraits () {
|
||||
const traitsDir = path.join(__dirname, '../../../rpc/src/v1/traits');
|
||||
|
||||
return fs.readdirSync(traitsDir)
|
||||
.filter((name) => name !== 'mod.rs' && /\.rs$/.test(name))
|
||||
.map((name) => fs.readFileSync(path.join(traitsDir, name)))
|
||||
.map(parseMethodsFromRust)
|
||||
.reduce((a, b) => a.concat(b));
|
||||
}
|
||||
|
||||
const rustMethods = {};
|
||||
|
||||
getMethodsFromRustTraits().sort().forEach((method) => {
|
||||
const [group, name] = method.split('_');
|
||||
|
||||
// Skip methods with malformed names
|
||||
if (group == null || name == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
rustMethods[group] = rustMethods[group] || {};
|
||||
rustMethods[group][name] = true;
|
||||
});
|
||||
|
||||
describe('jsonrpc/interfaces', () => {
|
||||
describe('Rust trait methods', () => {
|
||||
Object.keys(rustMethods).forEach((group) => {
|
||||
describe(group, () => {
|
||||
Object.keys(rustMethods[group]).forEach((name) => {
|
||||
describe(name, () => {
|
||||
it('has a defined JS interface', () => {
|
||||
expect(interfaces[group][name]).to.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Object.keys(interfaces).forEach((group) => {
|
||||
describe(group, () => {
|
||||
Object.keys(interfaces[group]).forEach((name) => {
|
||||
const method = interfaces[group][name];
|
||||
|
||||
describe(name, () => {
|
||||
if (!method.nodoc) {
|
||||
it('is present in Rust codebase', () => {
|
||||
expect(rustMethods[group][name]).to.exist;
|
||||
});
|
||||
}
|
||||
|
||||
it('has the correct interface', () => {
|
||||
expect(method.desc).to.be.a('string');
|
||||
expect(method.params).to.be.an('array');
|
||||
|
@ -15,15 +15,24 @@
|
||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.modal {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: 1.5em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: .5em !important;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-bottom: 1.5em;
|
||||
|
||||
.background {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
margin: 0 -1.5em;
|
||||
padding: 0.5em 1.5em;
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
@ -37,3 +46,26 @@
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
.selectIcon {
|
||||
position: absolute;
|
||||
right: 0.5em;
|
||||
top: 0.5em;
|
||||
}
|
||||
|
||||
.selected,
|
||||
.unselected {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.unselected {
|
||||
background: rgba(0, 0, 0, 0.4) !important;
|
||||
|
||||
.selectIcon {
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
background: rgba(255, 255, 255, 0.15) !important;
|
||||
}
|
||||
|
@ -14,14 +14,12 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Checkbox } from 'material-ui';
|
||||
import { List, ListItem } from 'material-ui/List';
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Modal, Button } from '~/ui';
|
||||
import { DoneIcon } from '~/ui/Icons';
|
||||
import { ContainerTitle, DappCard, Portal, SectionList } from '~/ui';
|
||||
import { CheckIcon } from '~/ui/Icons';
|
||||
|
||||
import styles from './addDapps.css';
|
||||
|
||||
@ -39,32 +37,23 @@ export default class AddDapps extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
actions={ [
|
||||
<Button
|
||||
icon={ <DoneIcon /> }
|
||||
key='done'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='dapps.add.button.done'
|
||||
defaultMessage='Done'
|
||||
/>
|
||||
}
|
||||
onClick={ store.closeModal }
|
||||
/>
|
||||
] }
|
||||
compact
|
||||
<Portal
|
||||
className={ styles.modal }
|
||||
onClose={ store.closeModal }
|
||||
open
|
||||
>
|
||||
<ContainerTitle
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='dapps.add.label'
|
||||
defaultMessage='visible applications'
|
||||
/>
|
||||
}
|
||||
visible
|
||||
>
|
||||
/>
|
||||
<div className={ styles.container }>
|
||||
<div className={ styles.warning } />
|
||||
{
|
||||
this.renderList(store.sortedLocal,
|
||||
this.renderList(store.sortedLocal, store.displayApps,
|
||||
<FormattedMessage
|
||||
id='dapps.add.local.label'
|
||||
defaultMessage='Applications locally available'
|
||||
@ -76,7 +65,7 @@ export default class AddDapps extends Component {
|
||||
)
|
||||
}
|
||||
{
|
||||
this.renderList(store.sortedBuiltin,
|
||||
this.renderList(store.sortedBuiltin, store.displayApps,
|
||||
<FormattedMessage
|
||||
id='dapps.add.builtin.label'
|
||||
defaultMessage='Applications bundled with Parity'
|
||||
@ -88,7 +77,7 @@ export default class AddDapps extends Component {
|
||||
)
|
||||
}
|
||||
{
|
||||
this.renderList(store.sortedNetwork,
|
||||
this.renderList(store.sortedNetwork, store.displayApps,
|
||||
<FormattedMessage
|
||||
id='dapps.add.network.label'
|
||||
defaultMessage='Applications on the global network'
|
||||
@ -99,11 +88,12 @@ export default class AddDapps extends Component {
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Modal>
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
renderList (items, header, byline) {
|
||||
renderList (items, visibleItems, header, byline) {
|
||||
if (!items || !items.length) {
|
||||
return null;
|
||||
}
|
||||
@ -114,41 +104,40 @@ export default class AddDapps extends Component {
|
||||
<div className={ styles.header }>{ header }</div>
|
||||
<div className={ styles.byline }>{ byline }</div>
|
||||
</div>
|
||||
<List>
|
||||
{ items.map(this.renderApp) }
|
||||
</List>
|
||||
<SectionList
|
||||
items={ items }
|
||||
noStretch
|
||||
renderItem={ this.renderApp }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderApp = (app) => {
|
||||
const { store } = this.props;
|
||||
const isHidden = !store.displayApps[app.id].visible;
|
||||
const isVisible = store.displayApps[app.id].visible;
|
||||
|
||||
const onCheck = () => {
|
||||
if (isHidden) {
|
||||
store.showApp(app.id);
|
||||
} else {
|
||||
const onClick = () => {
|
||||
if (isVisible) {
|
||||
store.hideApp(app.id);
|
||||
} else {
|
||||
store.showApp(app.id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
<DappCard
|
||||
app={ app }
|
||||
className={
|
||||
isVisible
|
||||
? styles.selected
|
||||
: styles.unselected
|
||||
}
|
||||
key={ app.id }
|
||||
leftCheckbox={
|
||||
<Checkbox
|
||||
checked={ !isHidden }
|
||||
onCheck={ onCheck }
|
||||
/>
|
||||
}
|
||||
primaryText={ app.name }
|
||||
secondaryText={
|
||||
<div className={ styles.description }>
|
||||
{ app.description }
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
onClick={ onClick }
|
||||
>
|
||||
<CheckIcon className={ styles.selectIcon } />
|
||||
</DappCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -33,13 +33,13 @@ describe('modals/AddDapps', () => {
|
||||
|
||||
it('does not render the modal with modalOpen = false', () => {
|
||||
expect(
|
||||
renderShallow({ modalOpen: false }).find('Connect(Modal)')
|
||||
renderShallow({ modalOpen: false }).find('Portal')
|
||||
).to.have.length(0);
|
||||
});
|
||||
|
||||
it('does render the modal with modalOpen = true', () => {
|
||||
expect(
|
||||
renderShallow({ modalOpen: true }).find('Connect(Modal)')
|
||||
renderShallow({ modalOpen: true }).find('Portal')
|
||||
).to.have.length(1);
|
||||
});
|
||||
});
|
||||
|
@ -30,6 +30,10 @@
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.terms {
|
||||
line-height: 1.3;
|
||||
opacity: .7;
|
||||
|
@ -31,19 +31,22 @@ import emailTermsOfService from '~/3rdparty/email-verification/terms-of-service'
|
||||
import { howSMSVerificationWorks, howEmailVerificationWorks } from '../how-it-works';
|
||||
import styles from './gatherData.css';
|
||||
|
||||
const boolOfError = PropTypes.oneOfType([ PropTypes.bool, PropTypes.instanceOf(Error) ]);
|
||||
|
||||
export default class GatherData extends Component {
|
||||
static propTypes = {
|
||||
fee: React.PropTypes.instanceOf(BigNumber),
|
||||
fields: PropTypes.array.isRequired,
|
||||
hasRequested: nullableProptype(PropTypes.bool.isRequired),
|
||||
accountHasRequested: nullableProptype(PropTypes.bool.isRequired),
|
||||
isServerRunning: nullableProptype(PropTypes.bool.isRequired),
|
||||
isVerified: nullableProptype(PropTypes.bool.isRequired),
|
||||
isAbleToRequest: nullableProptype(boolOfError.isRequired),
|
||||
accountIsVerified: nullableProptype(PropTypes.bool.isRequired),
|
||||
method: PropTypes.string.isRequired,
|
||||
setConsentGiven: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const { method, isVerified } = this.props;
|
||||
const { method, accountIsVerified } = this.props;
|
||||
const termsOfService = method === 'email' ? emailTermsOfService : smsTermsOfService;
|
||||
const howItWorks = method === 'email' ? howEmailVerificationWorks : howSMSVerificationWorks;
|
||||
|
||||
@ -55,6 +58,7 @@ export default class GatherData extends Component {
|
||||
{ this.renderCertified() }
|
||||
{ this.renderRequested() }
|
||||
{ this.renderFields() }
|
||||
{ this.renderIfAbleToRequest() }
|
||||
<Checkbox
|
||||
className={ styles.spacing }
|
||||
label={
|
||||
@ -63,7 +67,7 @@ export default class GatherData extends Component {
|
||||
defaultMessage='I agree to the terms and conditions below.'
|
||||
/>
|
||||
}
|
||||
disabled={ isVerified }
|
||||
disabled={ accountIsVerified }
|
||||
onCheck={ this.consentOnChange }
|
||||
/>
|
||||
<div className={ styles.terms }>{ termsOfService }</div>
|
||||
@ -145,27 +149,27 @@ export default class GatherData extends Component {
|
||||
}
|
||||
|
||||
renderCertified () {
|
||||
const { isVerified } = this.props;
|
||||
const { accountIsVerified } = this.props;
|
||||
|
||||
if (isVerified) {
|
||||
if (accountIsVerified) {
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<ErrorIcon />
|
||||
<p className={ styles.message }>
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.isVerified.true'
|
||||
id='ui.verification.gatherData.accountIsVerified.true'
|
||||
defaultMessage='Your account is already verified.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
} else if (isVerified === false) {
|
||||
} else if (accountIsVerified === false) {
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<SuccessIcon />
|
||||
<p className={ styles.message }>
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.isVerified.false'
|
||||
id='ui.verification.gatherData.accountIsVerified.false'
|
||||
defaultMessage='Your account is not verified yet.'
|
||||
/>
|
||||
</p>
|
||||
@ -175,7 +179,7 @@ export default class GatherData extends Component {
|
||||
return (
|
||||
<p className={ styles.message }>
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.isVerified.pending'
|
||||
id='ui.verification.gatherData.accountIsVerified.pending'
|
||||
defaultMessage='Checking if your account is verified…'
|
||||
/>
|
||||
</p>
|
||||
@ -183,33 +187,33 @@ export default class GatherData extends Component {
|
||||
}
|
||||
|
||||
renderRequested () {
|
||||
const { isVerified, hasRequested } = this.props;
|
||||
const { accountIsVerified, accountHasRequested } = this.props;
|
||||
|
||||
// If the account is verified, don't show that it has requested verification.
|
||||
if (isVerified) {
|
||||
if (accountIsVerified) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasRequested) {
|
||||
if (accountHasRequested) {
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<InfoIcon />
|
||||
<p className={ styles.message }>
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.hasRequested.true'
|
||||
defaultMessage='You already requested verification.'
|
||||
id='ui.verification.gatherData.accountHasRequested.true'
|
||||
defaultMessage='You already requested verification from this account.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
} else if (hasRequested === false) {
|
||||
} else if (accountHasRequested === false) {
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<SuccessIcon />
|
||||
<p className={ styles.message }>
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.hasRequested.false'
|
||||
defaultMessage='You did not request verification yet.'
|
||||
id='ui.verification.gatherData.accountHasRequested.false'
|
||||
defaultMessage='You did not request verification from this account yet.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
@ -218,7 +222,7 @@ export default class GatherData extends Component {
|
||||
return (
|
||||
<p className={ styles.message }>
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.hasRequested.pending'
|
||||
id='ui.verification.gatherData.accountHasRequested.pending'
|
||||
defaultMessage='Checking if you requested verification…'
|
||||
/>
|
||||
</p>
|
||||
@ -226,7 +230,7 @@ export default class GatherData extends Component {
|
||||
}
|
||||
|
||||
renderFields () {
|
||||
const { isVerified, fields } = this.props;
|
||||
const { accountIsVerified, fields } = this.props;
|
||||
|
||||
const rendered = fields.map((field) => {
|
||||
const onChange = (_, v) => {
|
||||
@ -236,11 +240,12 @@ export default class GatherData extends Component {
|
||||
|
||||
return (
|
||||
<Input
|
||||
className={ styles.field }
|
||||
key={ field.key }
|
||||
label={ field.label }
|
||||
hint={ field.hint }
|
||||
error={ field.error }
|
||||
disabled={ isVerified }
|
||||
disabled={ accountIsVerified }
|
||||
onChange={ onChange }
|
||||
onSubmit={ onSubmit }
|
||||
/>
|
||||
@ -250,6 +255,36 @@ export default class GatherData extends Component {
|
||||
return (<div>{rendered}</div>);
|
||||
}
|
||||
|
||||
renderIfAbleToRequest () {
|
||||
const { accountIsVerified, isAbleToRequest } = this.props;
|
||||
|
||||
// If the account is verified, don't show a warning.
|
||||
// If the client is able to send the request, don't show a warning
|
||||
if (accountIsVerified || isAbleToRequest === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isAbleToRequest === null) {
|
||||
return (
|
||||
<p className={ styles.message }>
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.isAbleToRequest.pending'
|
||||
defaultMessage='Validating your input…'
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
} else if (isAbleToRequest) {
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<ErrorIcon />
|
||||
<p className={ styles.message }>
|
||||
{ isAbleToRequest.message }
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
consentOnChange = (_, consentGiven) => {
|
||||
this.props.setConsentGiven(consentGiven);
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import { observable, computed, action } from 'mobx';
|
||||
import { sha3 } from '~/api/util/sha3';
|
||||
import { bytesToHex } from '~/api/util/format';
|
||||
|
||||
import EmailVerificationABI from '~/contracts/abi/email-verification.json';
|
||||
import VerificationStore, {
|
||||
@ -23,6 +24,8 @@ import VerificationStore, {
|
||||
} from './store';
|
||||
import { isServerRunning, hasReceivedCode, postToServer } from '~/3rdparty/email-verification';
|
||||
|
||||
const ZERO20 = '0x0000000000000000000000000000000000000000';
|
||||
|
||||
// name in the `BadgeReg.sol` contract
|
||||
const EMAIL_VERIFICATION = 'emailverification';
|
||||
|
||||
@ -44,9 +47,9 @@ export default class EmailVerificationStore extends VerificationStore {
|
||||
|
||||
switch (this.step) {
|
||||
case LOADING:
|
||||
return this.contract && this.fee && this.isVerified !== null && this.hasRequested !== null;
|
||||
return this.contract && this.fee && this.accountIsVerified !== null && this.accountHasRequested !== null;
|
||||
case QUERY_DATA:
|
||||
return this.isEmailValid && this.consentGiven;
|
||||
return this.isEmailValid && this.consentGiven && this.isAbleToRequest === true;
|
||||
case QUERY_CODE:
|
||||
return this.requestTx && this.isCodeValid === true;
|
||||
case POSTED_CONFIRMATION:
|
||||
@ -68,8 +71,53 @@ export default class EmailVerificationStore extends VerificationStore {
|
||||
return hasReceivedCode(this.email, this.account, this.isTestnet);
|
||||
}
|
||||
|
||||
// If the email has already been used for verification of another account,
|
||||
// we prevent the user from wasting ETH to request another verification.
|
||||
@action setIfAbleToRequest = () => {
|
||||
const { isEmailValid } = this;
|
||||
|
||||
if (!isEmailValid) {
|
||||
this.isAbleToRequest = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const { contract, email } = this;
|
||||
const emailHash = sha3.text(email);
|
||||
|
||||
this.isAbleToRequest = null;
|
||||
contract
|
||||
.instance.reverse
|
||||
.call({}, [ emailHash ])
|
||||
.then((address) => {
|
||||
if (address === ZERO20) {
|
||||
this.isAbleToRequest = true;
|
||||
} else {
|
||||
this.isAbleToRequest = new Error('Another account has been verified using this e-mail.');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.error = 'Failed to check if able to send request: ' + err.message;
|
||||
});
|
||||
}
|
||||
|
||||
// Determine the values relevant for checking if the last request contains
|
||||
// the same data as the current one.
|
||||
requestValues = () => [ sha3.text(this.email) ]
|
||||
|
||||
shallSkipRequest = (currentValues) => {
|
||||
const { accountHasRequested } = this;
|
||||
const lastRequest = this.lastRequestValues;
|
||||
|
||||
if (!accountHasRequested) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
// If the last email verification `request` for the selected address contains
|
||||
// the same email as the current one, don't send another request to save ETH.
|
||||
const skip = currentValues[0] === bytesToHex(lastRequest.emailHash.value);
|
||||
|
||||
return Promise.resolve(skip);
|
||||
}
|
||||
|
||||
@action setEmail = (email) => {
|
||||
this.email = email;
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ export default class SMSVerificationStore extends VerificationStore {
|
||||
|
||||
switch (this.step) {
|
||||
case LOADING:
|
||||
return this.contract && this.fee && this.isVerified !== null && this.hasRequested !== null;
|
||||
return this.contract && this.fee && this.accountIsVerified !== null && this.accountHasRequested !== null;
|
||||
case QUERY_DATA:
|
||||
return this.isNumberValid && this.consentGiven;
|
||||
case QUERY_CODE:
|
||||
@ -67,6 +67,18 @@ export default class SMSVerificationStore extends VerificationStore {
|
||||
return hasReceivedCode(this.number, this.account, this.isTestnet);
|
||||
}
|
||||
|
||||
// SMS verification events don't contain the phone number, so we will have to
|
||||
// send a new request every single time. See below.
|
||||
@action setIfAbleToRequest = () => {
|
||||
this.isAbleToRequest = true;
|
||||
}
|
||||
|
||||
// SMS verification `request` & `confirm` transactions and events don't contain the
|
||||
// phone number, so we will have to send a new request every single time. This may
|
||||
// cost the user more money, but given that it fails otherwise, it seems like a
|
||||
// reasonable tradeoff.
|
||||
shallSkipRequest = () => Promise.resolve(false)
|
||||
|
||||
@action setNumber = (number) => {
|
||||
this.number = number;
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import { sha3 } from '~/api/util/sha3';
|
||||
import Contract from '~/api/contract';
|
||||
import Contracts from '~/contracts';
|
||||
|
||||
import { checkIfVerified, checkIfRequested, awaitPuzzle } from '~/contracts/verification';
|
||||
import { checkIfVerified, findLastRequested, awaitPuzzle } from '~/contracts/verification';
|
||||
import { checkIfTxFailed, waitForConfirmations } from '~/util/tx';
|
||||
|
||||
export const LOADING = 'fetching-contract';
|
||||
@ -38,8 +38,10 @@ export default class VerificationStore {
|
||||
|
||||
@observable contract = null;
|
||||
@observable fee = null;
|
||||
@observable isVerified = null;
|
||||
@observable hasRequested = null;
|
||||
@observable accountIsVerified = null;
|
||||
@observable accountHasRequested = null;
|
||||
@observable isAbleToRequest = null;
|
||||
@observable lastRequestValues = null;
|
||||
@observable isServerRunning = null;
|
||||
@observable consentGiven = false;
|
||||
@observable requestTx = null;
|
||||
@ -68,6 +70,14 @@ export default class VerificationStore {
|
||||
console.error('verification: ' + this.error);
|
||||
}
|
||||
});
|
||||
|
||||
autorun(() => {
|
||||
if (this.step !== QUERY_DATA) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setIfAbleToRequest();
|
||||
});
|
||||
}
|
||||
|
||||
@action load = () => {
|
||||
@ -91,19 +101,20 @@ export default class VerificationStore {
|
||||
this.error = 'Failed to fetch the fee: ' + err.message;
|
||||
});
|
||||
|
||||
const isVerified = checkIfVerified(contract, account)
|
||||
.then((isVerified) => {
|
||||
this.isVerified = isVerified;
|
||||
const accountIsVerified = checkIfVerified(contract, account)
|
||||
.then((accountIsVerified) => {
|
||||
this.accountIsVerified = accountIsVerified;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.error = 'Failed to check if verified: ' + err.message;
|
||||
});
|
||||
|
||||
const hasRequested = checkIfRequested(contract, account)
|
||||
.then((txHash) => {
|
||||
this.hasRequested = !!txHash;
|
||||
if (txHash) {
|
||||
this.requestTx = txHash;
|
||||
const accountHasRequested = findLastRequested(contract, account)
|
||||
.then((log) => {
|
||||
this.accountHasRequested = !!log;
|
||||
if (log) {
|
||||
this.lastRequestValues = log.params;
|
||||
this.requestTx = log.transactionHash;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
@ -111,7 +122,7 @@ export default class VerificationStore {
|
||||
});
|
||||
|
||||
Promise
|
||||
.all([ isServerRunning, fee, isVerified, hasRequested ])
|
||||
.all([ isServerRunning, fee, accountIsVerified, accountHasRequested ])
|
||||
.then(() => {
|
||||
this.step = QUERY_DATA;
|
||||
});
|
||||
@ -150,24 +161,27 @@ export default class VerificationStore {
|
||||
requestValues = () => []
|
||||
|
||||
@action sendRequest = () => {
|
||||
const { api, account, contract, fee, hasRequested } = this;
|
||||
const { api, account, contract, fee } = this;
|
||||
|
||||
const request = contract.functions.find((fn) => fn.name === 'request');
|
||||
const options = { from: account, value: fee.toString() };
|
||||
const values = this.requestValues();
|
||||
|
||||
let chain = Promise.resolve();
|
||||
this.shallSkipRequest(values)
|
||||
.then((skipRequest) => {
|
||||
if (skipRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasRequested) {
|
||||
this.step = POSTING_REQUEST;
|
||||
chain = request.estimateGas(options, values)
|
||||
return request.estimateGas(options, values)
|
||||
.then((gas) => {
|
||||
options.gas = gas.mul(1.2).toFixed(0);
|
||||
return request.postTransaction(options, values);
|
||||
})
|
||||
.then((handle) => {
|
||||
// TODO: The "request rejected" error doesn't have any property to
|
||||
// distinguish it from other errors, so we can't give a meaningful error here.
|
||||
// The "request rejected" error doesn't have any property to distinguish
|
||||
// it from other errors, so we can't give a meaningful error here.
|
||||
return api.pollMethod('parity_checkRequest', handle);
|
||||
})
|
||||
.then((txHash) => {
|
||||
@ -181,9 +195,7 @@ export default class VerificationStore {
|
||||
return waitForConfirmations(api, txHash, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
chain
|
||||
})
|
||||
.then(() => this.checkIfReceivedCode())
|
||||
.then((hasReceived) => {
|
||||
if (hasReceived) {
|
||||
|
@ -15,6 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { observer } from 'mobx-react';
|
||||
import { observable } from 'mobx';
|
||||
@ -206,7 +207,7 @@ class Verification extends Component {
|
||||
|
||||
const {
|
||||
step,
|
||||
isServerRunning, fee, isVerified, hasRequested,
|
||||
isServerRunning, isAbleToRequest, fee, accountIsVerified, accountHasRequested,
|
||||
requestTx, isCodeValid, confirmationTx,
|
||||
setCode
|
||||
} = this.store;
|
||||
@ -223,17 +224,37 @@ class Verification extends Component {
|
||||
if (method === 'sms') {
|
||||
fields.push({
|
||||
key: 'number',
|
||||
label: 'phone number in international format',
|
||||
hint: 'the SMS will be sent to this number',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.phoneNumber.label'
|
||||
defaultMessage='phone number in international format'
|
||||
/>
|
||||
),
|
||||
hint: (
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.phoneNumber.hint'
|
||||
defaultMessage='the SMS will be sent to this number'
|
||||
/>
|
||||
),
|
||||
error: this.store.isNumberValid ? null : 'invalid number',
|
||||
onChange: this.store.setNumber
|
||||
});
|
||||
} else if (method === 'email') {
|
||||
fields.push({
|
||||
key: 'email',
|
||||
label: 'email address',
|
||||
hint: 'the code will be sent to this address',
|
||||
error: this.store.isEmailValid ? null : 'invalid email',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.email.label'
|
||||
defaultMessage='e-mail address'
|
||||
/>
|
||||
),
|
||||
hint: (
|
||||
<FormattedMessage
|
||||
id='ui.verification.gatherData.email.hint'
|
||||
defaultMessage='the code will be sent to this address'
|
||||
/>
|
||||
),
|
||||
error: this.store.isEmailValid ? null : 'invalid e-mail',
|
||||
onChange: this.store.setEmail
|
||||
});
|
||||
}
|
||||
@ -241,10 +262,12 @@ class Verification extends Component {
|
||||
return (
|
||||
<GatherData
|
||||
fee={ fee }
|
||||
hasRequested={ hasRequested }
|
||||
accountHasRequested={ accountHasRequested }
|
||||
isServerRunning={ isServerRunning }
|
||||
isVerified={ isVerified }
|
||||
method={ method } fields={ fields }
|
||||
isAbleToRequest={ isAbleToRequest }
|
||||
accountIsVerified={ accountIsVerified }
|
||||
method={ method }
|
||||
fields={ fields }
|
||||
setConsentGiven={ setConsentGiven }
|
||||
/>
|
||||
);
|
||||
|
@ -16,10 +16,10 @@
|
||||
|
||||
import AddAddress from './AddAddress';
|
||||
import AddContract from './AddContract';
|
||||
import AddDapps from './AddDapps';
|
||||
import CreateAccount from './CreateAccount';
|
||||
import CreateWallet from './CreateWallet';
|
||||
import DappPermissions from './DappPermissions';
|
||||
import DappsVisible from './AddDapps';
|
||||
import DeleteAccount from './DeleteAccount';
|
||||
import DeployContract from './DeployContract';
|
||||
import EditMeta from './EditMeta';
|
||||
@ -37,10 +37,10 @@ import WalletSettings from './WalletSettings';
|
||||
export {
|
||||
AddAddress,
|
||||
AddContract,
|
||||
AddDapps,
|
||||
CreateAccount,
|
||||
CreateWallet,
|
||||
DappPermissions,
|
||||
DappsVisible,
|
||||
DeleteAccount,
|
||||
DeployContract,
|
||||
EditMeta,
|
||||
|
@ -29,15 +29,14 @@ export default class Container extends Component {
|
||||
className: PropTypes.string,
|
||||
compact: PropTypes.bool,
|
||||
light: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
tabIndex: PropTypes.number,
|
||||
title: nodeOrStringProptype()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, className, compact, light, style, tabIndex } = this.props;
|
||||
const classes = `${styles.container} ${light ? styles.light : ''} ${className}`;
|
||||
|
||||
const { children, className, compact, light, onClick, style, tabIndex } = this.props;
|
||||
const props = {};
|
||||
|
||||
if (Number.isInteger(tabIndex)) {
|
||||
@ -45,8 +44,27 @@ export default class Container extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ classes } style={ style } { ...props }>
|
||||
<Card className={ compact ? styles.compact : styles.padded }>
|
||||
<div
|
||||
className={
|
||||
[
|
||||
styles.container,
|
||||
light
|
||||
? styles.light
|
||||
: '',
|
||||
className
|
||||
].join(' ')
|
||||
}
|
||||
style={ style }
|
||||
{ ...props }
|
||||
>
|
||||
<Card
|
||||
className={
|
||||
compact
|
||||
? styles.compact
|
||||
: styles.padded
|
||||
}
|
||||
onClick={ onClick }
|
||||
>
|
||||
{ this.renderTitle() }
|
||||
{ children }
|
||||
</Card>
|
||||
|
@ -17,59 +17,80 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { Container, ContainerTitle, DappIcon, Tags } from '~/ui';
|
||||
import Container, { Title as ContainerTitle } from '~/ui/Container';
|
||||
import DappIcon from '~/ui/DappIcon';
|
||||
import Tags from '~/ui/Tags';
|
||||
|
||||
import styles from './summary.css';
|
||||
import styles from './dappCard.css';
|
||||
|
||||
export default class Summary extends Component {
|
||||
export default class DappCard extends Component {
|
||||
static propTypes = {
|
||||
app: PropTypes.object.isRequired,
|
||||
children: PropTypes.node
|
||||
}
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
showLink: PropTypes.bool,
|
||||
showTags: PropTypes.bool
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
showLink: false,
|
||||
showTags: false
|
||||
};
|
||||
|
||||
render () {
|
||||
const { app } = this.props;
|
||||
const { app, children, className, onClick, showLink, showTags } = this.props;
|
||||
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const link = this.renderLink(app);
|
||||
|
||||
return (
|
||||
<Container className={ styles.container }>
|
||||
<Container
|
||||
className={
|
||||
[styles.container, className].join(' ')
|
||||
}
|
||||
onClick={ onClick }
|
||||
>
|
||||
<DappIcon
|
||||
app={ app }
|
||||
className={ styles.image }
|
||||
/>
|
||||
<Tags tags={ [app.type] } />
|
||||
<Tags
|
||||
tags={
|
||||
showTags
|
||||
? [app.type]
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<div className={ styles.description }>
|
||||
<ContainerTitle
|
||||
className={ styles.title }
|
||||
title={ link }
|
||||
title={
|
||||
showLink
|
||||
? this.renderLink(app)
|
||||
: app.name
|
||||
}
|
||||
byline={ app.description }
|
||||
/>
|
||||
<div className={ styles.author }>
|
||||
{ app.author }, v{ app.version }
|
||||
</div>
|
||||
{ this.props.children }
|
||||
{ children }
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
renderLink (app) {
|
||||
// Special case for web dapp
|
||||
if (app.url === 'web') {
|
||||
return (
|
||||
<Link to={ `/web` }>
|
||||
{ app.name }
|
||||
</Link>
|
||||
);
|
||||
<Link
|
||||
to={
|
||||
app.url === 'web'
|
||||
? '/web'
|
||||
: `/app/${app.id}`
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to={ `/app/${app.id}` }>
|
||||
>
|
||||
{ app.name }
|
||||
</Link>
|
||||
);
|
@ -14,4 +14,4 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
export default from './summary';
|
||||
export default from './dappCard';
|
@ -30,6 +30,7 @@ import Container, { Title as ContainerTitle } from './Container';
|
||||
import ContextProvider from './ContextProvider';
|
||||
import CopyToClipboard from './CopyToClipboard';
|
||||
import CurrencySymbol from './CurrencySymbol';
|
||||
import DappCard from './DappCard';
|
||||
import DappIcon from './DappIcon';
|
||||
import Editor from './Editor';
|
||||
import Errors from './Errors';
|
||||
@ -79,6 +80,7 @@ export {
|
||||
CopyToClipboard,
|
||||
CurrencySymbol,
|
||||
DappIcon,
|
||||
DappCard,
|
||||
Editor,
|
||||
Errors,
|
||||
FEATURES,
|
||||
|
@ -21,14 +21,13 @@ import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { AddDapps, DappPermissions } from '~/modals';
|
||||
import { DappPermissions, DappsVisible } from '~/modals';
|
||||
import PermissionStore from '~/modals/DappPermissions/store';
|
||||
import { Actionbar, Button, Page } from '~/ui';
|
||||
import { Actionbar, Button, DappCard, Page } from '~/ui';
|
||||
import { LockedIcon, VisibleIcon } from '~/ui/Icons';
|
||||
|
||||
import UrlButton from './UrlButton';
|
||||
import DappsStore from './dappsStore';
|
||||
import Summary from './Summary';
|
||||
|
||||
import styles from './dapps.css';
|
||||
|
||||
@ -82,8 +81,8 @@ class Dapps extends Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AddDapps store={ this.store } />
|
||||
<DappPermissions store={ this.permissionStore } />
|
||||
<DappsVisible store={ this.store } />
|
||||
<Actionbar
|
||||
className={ styles.toolbar }
|
||||
title={
|
||||
@ -146,7 +145,11 @@ class Dapps extends Component {
|
||||
className={ styles.item }
|
||||
key={ app.id }
|
||||
>
|
||||
<Summary app={ app } />
|
||||
<DappCard
|
||||
app={ app }
|
||||
showLink
|
||||
showTags
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -192,7 +192,7 @@ pub fn sign_and_dispatch<C, M>(client: &C, miner: &M, accounts: &AccountProvider
|
||||
{
|
||||
|
||||
let network_id = client.signing_network_id();
|
||||
let min_block = filled.min_block.clone();
|
||||
let condition = filled.condition.clone();
|
||||
let signed_transaction = sign_no_dispatch(client, miner, accounts, filled, password)?;
|
||||
|
||||
let (signed_transaction, token) = match signed_transaction {
|
||||
@ -201,7 +201,7 @@ pub fn sign_and_dispatch<C, M>(client: &C, miner: &M, accounts: &AccountProvider
|
||||
};
|
||||
|
||||
trace!(target: "miner", "send_transaction: dispatching tx: {} for network ID {:?}", rlp::encode(&signed_transaction).to_vec().pretty(), network_id);
|
||||
let pending_transaction = PendingTransaction::new(signed_transaction, min_block);
|
||||
let pending_transaction = PendingTransaction::new(signed_transaction, condition.map(Into::into));
|
||||
dispatch_transaction(&*client, &*miner, pending_transaction).map(|hash| {
|
||||
match token {
|
||||
Some(ref token) => WithToken::Yes(hash, token.clone()),
|
||||
@ -222,7 +222,7 @@ pub fn fill_optional_fields<C, M>(request: TransactionRequest, default_sender: A
|
||||
gas: request.gas.unwrap_or_else(|| miner.sensible_gas_limit()),
|
||||
value: request.value.unwrap_or_else(|| 0.into()),
|
||||
data: request.data.unwrap_or_else(Vec::new),
|
||||
min_block: request.min_block,
|
||||
condition: request.condition,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use util::{Address, U256, Bytes};
|
||||
use v1::types::TransactionCondition;
|
||||
|
||||
/// Transaction request coming from RPC
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq, Hash)]
|
||||
@ -33,8 +34,8 @@ pub struct TransactionRequest {
|
||||
pub data: Option<Bytes>,
|
||||
/// Transaction's nonce
|
||||
pub nonce: Option<U256>,
|
||||
/// Delay until this block if specified.
|
||||
pub min_block: Option<u64>,
|
||||
/// Delay until this condition is met.
|
||||
pub condition: Option<TransactionCondition>,
|
||||
}
|
||||
|
||||
/// Transaction request coming from RPC with default values filled in.
|
||||
@ -56,8 +57,8 @@ pub struct FilledTransactionRequest {
|
||||
pub data: Bytes,
|
||||
/// Transaction's nonce
|
||||
pub nonce: Option<U256>,
|
||||
/// Delay until this block if specified.
|
||||
pub min_block: Option<u64>,
|
||||
/// Delay until this condition is met.
|
||||
pub condition: Option<TransactionCondition>,
|
||||
}
|
||||
|
||||
impl From<FilledTransactionRequest> for TransactionRequest {
|
||||
@ -70,7 +71,7 @@ impl From<FilledTransactionRequest> for TransactionRequest {
|
||||
value: Some(r.value),
|
||||
data: Some(r.data),
|
||||
nonce: r.nonce,
|
||||
min_block: r.min_block,
|
||||
condition: r.condition,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -349,7 +349,7 @@ mod test {
|
||||
value: 10_000_000.into(),
|
||||
data: vec![],
|
||||
nonce: None,
|
||||
min_block: None,
|
||||
condition: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -130,7 +130,7 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
|
||||
.into_iter().collect::<HashSet<_>>();
|
||||
|
||||
let info = store.accounts_info().map_err(|e| errors::account("Could not fetch account info.", e))?;
|
||||
let other = store.addresses_info().expect("addresses_info always returns Ok; qed");
|
||||
let other = store.addresses_info();
|
||||
|
||||
Ok(info
|
||||
.into_iter()
|
||||
|
@ -51,13 +51,16 @@ impl<C> ParityAccountsClient<C> where C: MiningBlockChainClient {
|
||||
}
|
||||
|
||||
impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlockChainClient {
|
||||
fn all_accounts_info(&self) -> Result<BTreeMap<String, 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 info = store.accounts_info().map_err(|e| errors::account("Could not fetch account info.", e))?;
|
||||
let other = store.addresses_info().expect("addresses_info always returns Ok; qed");
|
||||
let other = store.addresses_info();
|
||||
|
||||
Ok(info.into_iter().chain(other.into_iter()).map(|(a, v)| {
|
||||
Ok(info
|
||||
.into_iter()
|
||||
.chain(other.into_iter())
|
||||
.map(|(address, v)| {
|
||||
let mut m = map![
|
||||
"name".to_owned() => v.name,
|
||||
"meta".to_owned() => v.meta
|
||||
@ -65,8 +68,10 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
|
||||
if let &Some(ref uuid) = &v.uuid {
|
||||
m.insert("uuid".to_owned(), format!("{}", uuid));
|
||||
}
|
||||
(format!("0x{}", a.hex()), m)
|
||||
}).collect())
|
||||
(address.into(), m)
|
||||
})
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
|
||||
fn new_account_from_phrase(&self, phrase: String, pass: String) -> Result<RpcH160, Error> {
|
||||
@ -132,8 +137,7 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
|
||||
let store = take_weak!(self.accounts);
|
||||
let addr: Address = addr.into();
|
||||
|
||||
store.remove_address(addr)
|
||||
.expect("remove_address always returns Ok; qed");
|
||||
store.remove_address(addr);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@ -143,8 +147,7 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
|
||||
let addr: Address = addr.into();
|
||||
|
||||
store.set_account_name(addr.clone(), name.clone())
|
||||
.or_else(|_| store.set_address_name(addr, name))
|
||||
.expect("set_address_name always returns Ok; qed");
|
||||
.unwrap_or_else(|_| store.set_address_name(addr, name));
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@ -154,8 +157,7 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock
|
||||
let addr: Address = addr.into();
|
||||
|
||||
store.set_account_meta(addr.clone(), meta.clone())
|
||||
.or_else(|_| store.set_address_meta(addr, meta))
|
||||
.expect("set_address_meta always returns Ok; qed");
|
||||
.unwrap_or_else(|_| store.set_address_meta(addr, meta));
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
|
@ -87,8 +87,8 @@ impl<C: 'static, M: 'static> SignerClient<C, M> where C: MiningBlockChainClient,
|
||||
if let Some(gas) = modification.gas {
|
||||
request.gas = gas.into();
|
||||
}
|
||||
if let Some(ref min_block) = modification.min_block {
|
||||
request.min_block = min_block.as_ref().and_then(|b| b.to_min_block_num());
|
||||
if let Some(ref condition) = modification.condition {
|
||||
request.condition = condition.clone().map(Into::into);
|
||||
}
|
||||
}
|
||||
let result = f(&*client, &*miner, &*accounts, payload);
|
||||
@ -160,7 +160,7 @@ impl<C: 'static, M: 'static> Signer for SignerClient<C, M> where C: MiningBlockC
|
||||
|
||||
// Dispatch if everything is ok
|
||||
if sender_matches && data_matches && value_matches && nonce_matches {
|
||||
let pending_transaction = PendingTransaction::new(signed_transaction, request.min_block);
|
||||
let pending_transaction = PendingTransaction::new(signed_transaction, request.condition.map(Into::into));
|
||||
dispatch_transaction(&*client, &*miner, pending_transaction)
|
||||
.map(Into::into)
|
||||
.map(ConfirmationResponse::SendTransaction)
|
||||
|
@ -212,7 +212,7 @@ impl MinerService for TestMinerService {
|
||||
self.local_transactions.lock().iter().map(|(hash, stats)| (*hash, stats.clone())).collect()
|
||||
}
|
||||
|
||||
fn ready_transactions(&self, _best_block: BlockNumber) -> Vec<PendingTransaction> {
|
||||
fn ready_transactions(&self, _best_block: BlockNumber, _best_timestamp: u64) -> Vec<PendingTransaction> {
|
||||
self.pending_transactions.lock().values().cloned().map(Into::into).collect()
|
||||
}
|
||||
|
||||
|
@ -365,6 +365,8 @@ fn rpc_eth_accounts() {
|
||||
let tester = EthTester::default();
|
||||
let address = tester.accounts_provider.new_account("").unwrap();
|
||||
tester.accounts_provider.set_new_dapps_whitelist(None).unwrap();
|
||||
tester.accounts_provider.set_address_name(1.into(), "1".into());
|
||||
tester.accounts_provider.set_address_name(10.into(), "10".into());
|
||||
|
||||
// with current policy it should return the account
|
||||
let request = r#"{"jsonrpc": "2.0", "method": "eth_accounts", "params": [], "id": 1}"#;
|
||||
@ -514,7 +516,7 @@ fn rpc_eth_pending_transaction_by_hash() {
|
||||
tester.miner.pending_transactions.lock().insert(H256::zero(), tx);
|
||||
}
|
||||
|
||||
let response = r#"{"jsonrpc":"2.0","result":{"blockHash":null,"blockNumber":null,"creates":null,"from":"0x0f65fe9276bc9a24ae7083ae28e2660ef72df99e","gas":"0x5208","gasPrice":"0x1","hash":"0x41df922fd0d4766fcc02e161f8295ec28522f329ae487f14d811e4b64c8d6e31","input":"0x","minBlock":null,"networkId":null,"nonce":"0x0","publicKey":"0x7ae46da747962c2ee46825839c1ef9298e3bd2e70ca2938495c3693a485ec3eaa8f196327881090ff64cf4fbb0a48485d4f83098e189ed3b7a87d5941b59f789","r":"0x48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353","raw":"0xf85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804","s":"0xefffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804","standardV":"0x0","to":"0x095e7baea6a6c7c4c2dfeb977efac326af552d87","transactionIndex":null,"v":"0x1b","value":"0xa"},"id":1}"#;
|
||||
let response = r#"{"jsonrpc":"2.0","result":{"blockHash":null,"blockNumber":null,"condition":null,"creates":null,"from":"0x0f65fe9276bc9a24ae7083ae28e2660ef72df99e","gas":"0x5208","gasPrice":"0x1","hash":"0x41df922fd0d4766fcc02e161f8295ec28522f329ae487f14d811e4b64c8d6e31","input":"0x","networkId":null,"nonce":"0x0","publicKey":"0x7ae46da747962c2ee46825839c1ef9298e3bd2e70ca2938495c3693a485ec3eaa8f196327881090ff64cf4fbb0a48485d4f83098e189ed3b7a87d5941b59f789","r":"0x48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353","raw":"0xf85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804","s":"0xefffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804","standardV":"0x0","to":"0x095e7baea6a6c7c4c2dfeb977efac326af552d87","transactionIndex":null,"v":"0x1b","value":"0xa"},"id":1}"#;
|
||||
let request = r#"{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "eth_getTransactionByHash",
|
||||
@ -830,12 +832,11 @@ fn rpc_eth_sign_transaction() {
|
||||
let response = r#"{"jsonrpc":"2.0","result":{"#.to_owned() +
|
||||
r#""raw":"0x"# + &rlp.to_hex() + r#"","# +
|
||||
r#""tx":{"# +
|
||||
r#""blockHash":null,"blockNumber":null,"creates":null,"# +
|
||||
r#""blockHash":null,"blockNumber":null,"condition":null,"creates":null,"# +
|
||||
&format!("\"from\":\"0x{:?}\",", &address) +
|
||||
r#""gas":"0x76c0","gasPrice":"0x9184e72a000","# +
|
||||
&format!("\"hash\":\"0x{:?}\",", t.hash()) +
|
||||
r#""input":"0x","# +
|
||||
r#""minBlock":null,"# +
|
||||
&format!("\"networkId\":{},", t.network_id().map_or("null".to_owned(), |n| format!("{}", n))) +
|
||||
r#""nonce":"0x1","# +
|
||||
&format!("\"publicKey\":\"0x{:?}\",", t.recover_public().unwrap()) +
|
||||
|
@ -120,10 +120,11 @@ fn should_be_able_to_set_meta() {
|
||||
fn rpc_parity_set_and_get_dapps_accounts() {
|
||||
// given
|
||||
let tester = setup();
|
||||
tester.accounts.set_address_name(10.into(), "10".into());
|
||||
assert_eq!(tester.accounts.dapps_addresses("app1".into()).unwrap(), vec![]);
|
||||
|
||||
// when
|
||||
let request = r#"{"jsonrpc": "2.0", "method": "parity_setDappsAddresses","params":["app1",["0x000000000000000000000000000000000000000a"]], "id": 1}"#;
|
||||
let request = r#"{"jsonrpc": "2.0", "method": "parity_setDappsAddresses","params":["app1",["0x000000000000000000000000000000000000000a","0x0000000000000000000000000000000000000001"]], "id": 1}"#;
|
||||
let response = r#"{"jsonrpc":"2.0","result":true,"id":1}"#;
|
||||
assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned()));
|
||||
|
||||
|
@ -85,7 +85,7 @@ fn should_return_list_of_items_to_confirm() {
|
||||
value: U256::from(1),
|
||||
data: vec![],
|
||||
nonce: None,
|
||||
min_block: None,
|
||||
condition: None,
|
||||
})).unwrap();
|
||||
tester.signer.add_request(ConfirmationPayload::Signature(1.into(), vec![5].into())).unwrap();
|
||||
|
||||
@ -93,7 +93,7 @@ fn should_return_list_of_items_to_confirm() {
|
||||
let request = r#"{"jsonrpc":"2.0","method":"signer_requestsToConfirm","params":[],"id":1}"#;
|
||||
let response = concat!(
|
||||
r#"{"jsonrpc":"2.0","result":["#,
|
||||
r#"{"id":"0x1","payload":{"sendTransaction":{"data":"0x","from":"0x0000000000000000000000000000000000000001","gas":"0x989680","gasPrice":"0x2710","minBlock":null,"nonce":null,"to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","value":"0x1"}}},"#,
|
||||
r#"{"id":"0x1","payload":{"sendTransaction":{"condition":null,"data":"0x","from":"0x0000000000000000000000000000000000000001","gas":"0x989680","gasPrice":"0x2710","nonce":null,"to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","value":"0x1"}}},"#,
|
||||
r#"{"id":"0x2","payload":{"sign":{"address":"0x0000000000000000000000000000000000000001","data":"0x05"}}}"#,
|
||||
r#"],"id":1}"#
|
||||
);
|
||||
@ -116,7 +116,7 @@ fn should_reject_transaction_from_queue_without_dispatching() {
|
||||
value: U256::from(1),
|
||||
data: vec![],
|
||||
nonce: None,
|
||||
min_block: None,
|
||||
condition: None,
|
||||
})).unwrap();
|
||||
assert_eq!(tester.signer.requests().len(), 1);
|
||||
|
||||
@ -143,7 +143,7 @@ fn should_not_remove_transaction_if_password_is_invalid() {
|
||||
value: U256::from(1),
|
||||
data: vec![],
|
||||
nonce: None,
|
||||
min_block: None,
|
||||
condition: None,
|
||||
})).unwrap();
|
||||
assert_eq!(tester.signer.requests().len(), 1);
|
||||
|
||||
@ -187,7 +187,7 @@ fn should_confirm_transaction_and_dispatch() {
|
||||
value: U256::from(1),
|
||||
data: vec![],
|
||||
nonce: None,
|
||||
min_block: None,
|
||||
condition: None,
|
||||
})).unwrap();
|
||||
|
||||
let t = Transaction {
|
||||
@ -233,7 +233,7 @@ fn should_alter_the_sender_and_nonce() {
|
||||
value: U256::from(1),
|
||||
data: vec![],
|
||||
nonce: Some(10.into()),
|
||||
min_block: None,
|
||||
condition: None,
|
||||
})).unwrap();
|
||||
|
||||
let t = Transaction {
|
||||
@ -283,7 +283,7 @@ fn should_confirm_transaction_with_token() {
|
||||
value: U256::from(1),
|
||||
data: vec![],
|
||||
nonce: None,
|
||||
min_block: None,
|
||||
condition: None,
|
||||
})).unwrap();
|
||||
|
||||
let t = Transaction {
|
||||
@ -332,7 +332,7 @@ fn should_confirm_transaction_with_rlp() {
|
||||
value: U256::from(1),
|
||||
data: vec![],
|
||||
nonce: None,
|
||||
min_block: None,
|
||||
condition: None,
|
||||
})).unwrap();
|
||||
|
||||
let t = Transaction {
|
||||
@ -380,7 +380,7 @@ fn should_return_error_when_sender_does_not_match() {
|
||||
value: U256::from(1),
|
||||
data: vec![],
|
||||
nonce: None,
|
||||
min_block: None,
|
||||
condition: None,
|
||||
})).unwrap();
|
||||
|
||||
let t = Transaction {
|
||||
|
@ -277,12 +277,11 @@ fn should_add_sign_transaction_to_the_queue() {
|
||||
let response = r#"{"jsonrpc":"2.0","result":{"#.to_owned() +
|
||||
r#""raw":"0x"# + &rlp.to_hex() + r#"","# +
|
||||
r#""tx":{"# +
|
||||
r#""blockHash":null,"blockNumber":null,"creates":null,"# +
|
||||
r#""blockHash":null,"blockNumber":null,"condition":null,"creates":null,"# +
|
||||
&format!("\"from\":\"0x{:?}\",", &address) +
|
||||
r#""gas":"0x76c0","gasPrice":"0x9184e72a000","# +
|
||||
&format!("\"hash\":\"0x{:?}\",", t.hash()) +
|
||||
r#""input":"0x","# +
|
||||
r#""minBlock":null,"# +
|
||||
&format!("\"networkId\":{},", t.network_id().map_or("null".to_owned(), |n| format!("{}", n))) +
|
||||
r#""nonce":"0x1","# +
|
||||
&format!("\"publicKey\":\"0x{:?}\",", t.public_key()) +
|
||||
|
@ -25,7 +25,7 @@ build_rpc_trait! {
|
||||
pub trait ParityAccounts {
|
||||
/// Returns accounts information.
|
||||
#[rpc(name = "parity_allAccountsInfo")]
|
||||
fn all_accounts_info(&self) -> Result<BTreeMap<String, BTreeMap<String, String>>, Error>;
|
||||
fn all_accounts_info(&self) -> Result<BTreeMap<H160, BTreeMap<String, String>>, Error>;
|
||||
|
||||
/// Creates new account from the given phrase using standard brainwallet mechanism.
|
||||
/// Second parameter is password for the new account.
|
||||
|
@ -139,7 +139,7 @@ mod tests {
|
||||
fn test_serialize_block_transactions() {
|
||||
let t = BlockTransactions::Full(vec![Transaction::default()]);
|
||||
let serialized = serde_json::to_string(&t).unwrap();
|
||||
assert_eq!(serialized, r#"[{"hash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0","blockHash":null,"blockNumber":null,"transactionIndex":null,"from":"0x0000000000000000000000000000000000000000","to":null,"value":"0x0","gasPrice":"0x0","gas":"0x0","input":"0x","creates":null,"raw":"0x","publicKey":null,"networkId":null,"standardV":"0x0","v":"0x0","r":"0x0","s":"0x0","minBlock":null}]"#);
|
||||
assert_eq!(serialized, r#"[{"hash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0","blockHash":null,"blockNumber":null,"transactionIndex":null,"from":"0x0000000000000000000000000000000000000000","to":null,"value":"0x0","gasPrice":"0x0","gas":"0x0","input":"0x","creates":null,"raw":"0x","publicKey":null,"networkId":null,"standardV":"0x0","v":"0x0","r":"0x0","s":"0x0","condition":null}]"#);
|
||||
|
||||
let t = BlockTransactions::Hashes(vec![H256::default().into()]);
|
||||
let serialized = serde_json::to_string(&t).unwrap();
|
||||
|
@ -21,7 +21,7 @@ use serde::{Serialize, Serializer};
|
||||
use util::log::Colour;
|
||||
use util::bytes::ToPretty;
|
||||
|
||||
use v1::types::{U256, TransactionRequest, RichRawTransaction, H160, H256, H520, Bytes, BlockNumber};
|
||||
use v1::types::{U256, TransactionRequest, RichRawTransaction, H160, H256, H520, Bytes, TransactionCondition};
|
||||
use v1::helpers;
|
||||
|
||||
/// Confirmation waiting in a queue
|
||||
@ -196,9 +196,8 @@ pub struct TransactionModification {
|
||||
pub gas_price: Option<U256>,
|
||||
/// Modified gas
|
||||
pub gas: Option<U256>,
|
||||
/// Modified min block
|
||||
#[serde(rename="minBlock")]
|
||||
pub min_block: Option<Option<BlockNumber>>,
|
||||
/// Modified transaction condition.
|
||||
pub condition: Option<Option<TransactionCondition>>,
|
||||
}
|
||||
|
||||
/// Represents two possible return values.
|
||||
@ -240,7 +239,7 @@ impl<A, B> Serialize for Either<A, B> where
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
use serde_json;
|
||||
use v1::types::{U256, H256, BlockNumber};
|
||||
use v1::types::{U256, H256, TransactionCondition};
|
||||
use v1::helpers;
|
||||
use super::*;
|
||||
|
||||
@ -274,13 +273,13 @@ mod tests {
|
||||
value: 100_000.into(),
|
||||
data: vec![1, 2, 3],
|
||||
nonce: Some(1.into()),
|
||||
min_block: None,
|
||||
condition: None,
|
||||
}),
|
||||
};
|
||||
|
||||
// when
|
||||
let res = serde_json::to_string(&ConfirmationRequest::from(request));
|
||||
let expected = r#"{"id":"0xf","payload":{"sendTransaction":{"from":"0x0000000000000000000000000000000000000000","to":null,"gasPrice":"0x2710","gas":"0x3a98","value":"0x186a0","data":"0x010203","nonce":"0x1","minBlock":null}}}"#;
|
||||
let expected = r#"{"id":"0xf","payload":{"sendTransaction":{"from":"0x0000000000000000000000000000000000000000","to":null,"gasPrice":"0x2710","gas":"0x3a98","value":"0x186a0","data":"0x010203","nonce":"0x1","condition":null}}}"#;
|
||||
|
||||
// then
|
||||
assert_eq!(res.unwrap(), expected.to_owned());
|
||||
@ -300,13 +299,13 @@ mod tests {
|
||||
value: 100_000.into(),
|
||||
data: vec![1, 2, 3],
|
||||
nonce: Some(1.into()),
|
||||
min_block: None,
|
||||
condition: None,
|
||||
}),
|
||||
};
|
||||
|
||||
// when
|
||||
let res = serde_json::to_string(&ConfirmationRequest::from(request));
|
||||
let expected = r#"{"id":"0xf","payload":{"signTransaction":{"from":"0x0000000000000000000000000000000000000000","to":null,"gasPrice":"0x2710","gas":"0x3a98","value":"0x186a0","data":"0x010203","nonce":"0x1","minBlock":null}}}"#;
|
||||
let expected = r#"{"id":"0xf","payload":{"signTransaction":{"from":"0x0000000000000000000000000000000000000000","to":null,"gasPrice":"0x2710","gas":"0x3a98","value":"0x186a0","data":"0x010203","nonce":"0x1","condition":null}}}"#;
|
||||
|
||||
// then
|
||||
assert_eq!(res.unwrap(), expected.to_owned());
|
||||
@ -336,7 +335,7 @@ mod tests {
|
||||
let s1 = r#"{
|
||||
"sender": "0x000000000000000000000000000000000000000a",
|
||||
"gasPrice":"0xba43b7400",
|
||||
"minBlock":"0x42"
|
||||
"condition": { "block": 66 }
|
||||
}"#;
|
||||
let s2 = r#"{"gas": "0x1233"}"#;
|
||||
let s3 = r#"{}"#;
|
||||
@ -351,19 +350,19 @@ mod tests {
|
||||
sender: Some(10.into()),
|
||||
gas_price: Some(U256::from_str("0ba43b7400").unwrap()),
|
||||
gas: None,
|
||||
min_block: Some(Some(BlockNumber::Num(0x42))),
|
||||
condition: Some(Some(TransactionCondition::Number(0x42))),
|
||||
});
|
||||
assert_eq!(res2, TransactionModification {
|
||||
sender: None,
|
||||
gas_price: None,
|
||||
gas: Some(U256::from_str("1233").unwrap()),
|
||||
min_block: None,
|
||||
condition: None,
|
||||
});
|
||||
assert_eq!(res3, TransactionModification {
|
||||
sender: None,
|
||||
gas_price: None,
|
||||
gas: None,
|
||||
min_block: None,
|
||||
condition: None,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,7 @@ mod log;
|
||||
mod sync;
|
||||
mod transaction;
|
||||
mod transaction_request;
|
||||
mod transaction_condition;
|
||||
mod receipt;
|
||||
mod rpc_settings;
|
||||
mod trace;
|
||||
@ -55,6 +56,7 @@ pub use self::sync::{
|
||||
};
|
||||
pub use self::transaction::{Transaction, RichRawTransaction, LocalTransactionStatus};
|
||||
pub use self::transaction_request::TransactionRequest;
|
||||
pub use self::transaction_condition::TransactionCondition;
|
||||
pub use self::receipt::Receipt;
|
||||
pub use self::rpc_settings::RpcSettings;
|
||||
pub use self::trace::{LocalizedTrace, TraceResults};
|
||||
|
@ -19,7 +19,7 @@ use ethcore::miner;
|
||||
use ethcore::contract_address;
|
||||
use ethcore::transaction::{LocalizedTransaction, Action, PendingTransaction, SignedTransaction};
|
||||
use v1::helpers::errors;
|
||||
use v1::types::{Bytes, H160, H256, U256, H512, BlockNumber};
|
||||
use v1::types::{Bytes, H160, H256, U256, H512, TransactionCondition};
|
||||
|
||||
/// Transaction
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize)]
|
||||
@ -70,8 +70,7 @@ pub struct Transaction {
|
||||
/// The S field of the signature.
|
||||
pub s: U256,
|
||||
/// Transaction activates at specified block.
|
||||
#[serde(rename="minBlock")]
|
||||
pub min_block: Option<BlockNumber>,
|
||||
pub condition: Option<TransactionCondition>,
|
||||
}
|
||||
|
||||
/// Local Transaction Status
|
||||
@ -190,7 +189,7 @@ impl From<LocalizedTransaction> for Transaction {
|
||||
v: t.original_v().into(),
|
||||
r: signature.r().into(),
|
||||
s: signature.s().into(),
|
||||
min_block: None,
|
||||
condition: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -224,7 +223,7 @@ impl From<SignedTransaction> for Transaction {
|
||||
v: t.original_v().into(),
|
||||
r: signature.r().into(),
|
||||
s: signature.s().into(),
|
||||
min_block: None,
|
||||
condition: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -232,7 +231,7 @@ impl From<SignedTransaction> for Transaction {
|
||||
impl From<PendingTransaction> for Transaction {
|
||||
fn from(t: PendingTransaction) -> Transaction {
|
||||
let mut r = Transaction::from(t.transaction);
|
||||
r.min_block = t.min_block.map(|b| BlockNumber::Num(b));
|
||||
r.condition = t.condition.map(|b| b.into());
|
||||
r
|
||||
}
|
||||
}
|
||||
@ -261,7 +260,7 @@ mod tests {
|
||||
fn test_transaction_serialize() {
|
||||
let t = Transaction::default();
|
||||
let serialized = serde_json::to_string(&t).unwrap();
|
||||
assert_eq!(serialized, r#"{"hash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0","blockHash":null,"blockNumber":null,"transactionIndex":null,"from":"0x0000000000000000000000000000000000000000","to":null,"value":"0x0","gasPrice":"0x0","gas":"0x0","input":"0x","creates":null,"raw":"0x","publicKey":null,"networkId":null,"standardV":"0x0","v":"0x0","r":"0x0","s":"0x0","minBlock":null}"#);
|
||||
assert_eq!(serialized, r#"{"hash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0","blockHash":null,"blockNumber":null,"transactionIndex":null,"from":"0x0000000000000000000000000000000000000000","to":null,"value":"0x0","gasPrice":"0x0","gas":"0x0","input":"0x","creates":null,"raw":"0x","publicKey":null,"networkId":null,"standardV":"0x0","v":"0x0","r":"0x0","s":"0x0","condition":null}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
67
rpc/src/v1/types/transaction_condition.rs
Normal file
67
rpc/src/v1/types/transaction_condition.rs
Normal file
@ -0,0 +1,67 @@
|
||||
// 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/>.
|
||||
|
||||
use ethcore;
|
||||
|
||||
/// Represents condition on minimum block number or block timestamp.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub enum TransactionCondition {
|
||||
/// Valid at this minimum block number.
|
||||
#[serde(rename="block")]
|
||||
Number(u64),
|
||||
/// Valid at given unix time.
|
||||
#[serde(rename="time")]
|
||||
Timestamp(u64),
|
||||
}
|
||||
|
||||
impl Into<ethcore::transaction::Condition> for TransactionCondition {
|
||||
fn into(self) -> ethcore::transaction::Condition {
|
||||
match self {
|
||||
TransactionCondition::Number(n) => ethcore::transaction::Condition::Number(n),
|
||||
TransactionCondition::Timestamp(n) => ethcore::transaction::Condition::Timestamp(n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ethcore::transaction::Condition> for TransactionCondition {
|
||||
fn from(condition: ethcore::transaction::Condition) -> Self {
|
||||
match condition {
|
||||
ethcore::transaction::Condition::Number(n) => TransactionCondition::Number(n),
|
||||
ethcore::transaction::Condition::Timestamp(n) => TransactionCondition::Timestamp(n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ethcore;
|
||||
use super::*;
|
||||
use serde_json;
|
||||
|
||||
#[test]
|
||||
fn condition_deserialization() {
|
||||
let s = r#"[{ "block": 51 }, { "time": 10 }]"#;
|
||||
let deserialized: Vec<TransactionCondition> = serde_json::from_str(s).unwrap();
|
||||
assert_eq!(deserialized, vec![TransactionCondition::Number(51), TransactionCondition::Timestamp(10)])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn condition_into() {
|
||||
assert_eq!(ethcore::transaction::Condition::Number(100), TransactionCondition::Number(100).into());
|
||||
assert_eq!(ethcore::transaction::Condition::Timestamp(100), TransactionCondition::Timestamp(100).into());
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
//! `TransactionRequest` type
|
||||
|
||||
use v1::types::{Bytes, H160, U256, BlockNumber};
|
||||
use v1::types::{Bytes, H160, U256, TransactionCondition};
|
||||
use v1::helpers;
|
||||
use util::log::Colour;
|
||||
|
||||
@ -41,9 +41,8 @@ pub struct TransactionRequest {
|
||||
pub data: Option<Bytes>,
|
||||
/// Transaction's nonce
|
||||
pub nonce: Option<U256>,
|
||||
/// Delay until this block if specified.
|
||||
#[serde(rename="minBlock")]
|
||||
pub min_block: Option<BlockNumber>,
|
||||
/// Delay until this block condition.
|
||||
pub condition: Option<TransactionCondition>,
|
||||
}
|
||||
|
||||
pub fn format_ether(i: U256) -> String {
|
||||
@ -93,7 +92,7 @@ impl From<helpers::TransactionRequest> for TransactionRequest {
|
||||
value: r.value.map(Into::into),
|
||||
data: r.data.map(Into::into),
|
||||
nonce: r.nonce.map(Into::into),
|
||||
min_block: r.min_block.map(|b| BlockNumber::Num(b)),
|
||||
condition: r.condition.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -108,7 +107,7 @@ impl From<helpers::FilledTransactionRequest> for TransactionRequest {
|
||||
value: Some(r.value.into()),
|
||||
data: Some(r.data.into()),
|
||||
nonce: r.nonce.map(Into::into),
|
||||
min_block: r.min_block.map(|b| BlockNumber::Num(b)),
|
||||
condition: r.condition.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -123,7 +122,7 @@ impl Into<helpers::TransactionRequest> for TransactionRequest {
|
||||
value: self.value.map(Into::into),
|
||||
data: self.data.map(Into::into),
|
||||
nonce: self.nonce.map(Into::into),
|
||||
min_block: self.min_block.and_then(|b| b.to_min_block_num()),
|
||||
condition: self.condition.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -134,7 +133,7 @@ mod tests {
|
||||
use std::str::FromStr;
|
||||
use rustc_serialize::hex::FromHex;
|
||||
use serde_json;
|
||||
use v1::types::{U256, H160, BlockNumber};
|
||||
use v1::types::{U256, H160, TransactionCondition};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@ -147,7 +146,7 @@ mod tests {
|
||||
"value":"0x3",
|
||||
"data":"0x123456",
|
||||
"nonce":"0x4",
|
||||
"minBlock":"0x13"
|
||||
"condition": { "block": 19 }
|
||||
}"#;
|
||||
let deserialized: TransactionRequest = serde_json::from_str(s).unwrap();
|
||||
|
||||
@ -159,7 +158,7 @@ mod tests {
|
||||
value: Some(U256::from(3)),
|
||||
data: Some(vec![0x12, 0x34, 0x56].into()),
|
||||
nonce: Some(U256::from(4)),
|
||||
min_block: Some(BlockNumber::Num(0x13)),
|
||||
condition: Some(TransactionCondition::Number(0x13)),
|
||||
});
|
||||
}
|
||||
|
||||
@ -183,7 +182,7 @@ mod tests {
|
||||
value: Some(U256::from_str("9184e72a").unwrap()),
|
||||
data: Some("d46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675".from_hex().unwrap().into()),
|
||||
nonce: None,
|
||||
min_block: None,
|
||||
condition: None,
|
||||
});
|
||||
}
|
||||
|
||||
@ -200,7 +199,7 @@ mod tests {
|
||||
value: None,
|
||||
data: None,
|
||||
nonce: None,
|
||||
min_block: None,
|
||||
condition: None,
|
||||
});
|
||||
}
|
||||
|
||||
@ -224,7 +223,7 @@ mod tests {
|
||||
value: None,
|
||||
data: Some(vec![0x85, 0x95, 0xba, 0xb1].into()),
|
||||
nonce: None,
|
||||
min_block: None,
|
||||
condition: None,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
use client::{Rpc, RpcError};
|
||||
use rpc::v1::types::{ConfirmationRequest, TransactionModification, U256, BlockNumber};
|
||||
use rpc::v1::types::{ConfirmationRequest, TransactionModification, U256, TransactionCondition};
|
||||
use serde_json::{Value as JsonValue, to_value};
|
||||
use std::path::PathBuf;
|
||||
use futures::{BoxFuture, Canceled};
|
||||
@ -22,13 +22,13 @@ impl SignerRpc {
|
||||
id: U256,
|
||||
new_gas: Option<U256>,
|
||||
new_gas_price: Option<U256>,
|
||||
new_min_block: Option<Option<BlockNumber>>,
|
||||
new_condition: Option<Option<TransactionCondition>>,
|
||||
pwd: &str
|
||||
) -> BoxFuture<Result<U256, RpcError>, Canceled>
|
||||
{
|
||||
self.rpc.request("signer_confirmRequest", vec![
|
||||
to_value(&format!("{:#x}", id)),
|
||||
to_value(&TransactionModification { sender: None, gas_price: new_gas_price, gas: new_gas, min_block: new_min_block }),
|
||||
to_value(&TransactionModification { sender: None, gas_price: new_gas_price, gas: new_gas, condition: new_condition }),
|
||||
to_value(&pwd),
|
||||
])
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ echo "Homepage: https://ethcore.io" >> $control
|
||||
echo "Vcs-Git: git://github.com/ethcore/parity.git" >> $control
|
||||
echo "Vcs-Browser: https://github.com/ethcore/parity" >> $control
|
||||
echo "Architecture: $1" >> $control
|
||||
echo "Depends: libssl1.0.0" >> $control
|
||||
echo "Depends: libssl1.0.0 (>=1.0.0)" >> $control
|
||||
echo "Description: Ethereum network client by Ethcore" >> $control
|
||||
#build .deb package
|
||||
|
||||
|
@ -21,7 +21,7 @@ rust-crypto = "0.2.34"
|
||||
elastic-array = { git = "https://github.com/ethcore/elastic-array" }
|
||||
rlp = { path = "rlp" }
|
||||
heapsize = { version = "0.3", features = ["unstable"] }
|
||||
itertools = "0.4"
|
||||
itertools = "0.5"
|
||||
sha3 = { path = "sha3" }
|
||||
clippy = { version = "0.0.103", optional = true}
|
||||
ethcore-devtools = { path = "../devtools" }
|
||||
|
@ -9,8 +9,9 @@ authors = ["Parity Technologies <admin@parity.io>"]
|
||||
[dependencies]
|
||||
futures = "0.1"
|
||||
futures-cpupool = "0.1"
|
||||
parking_lot = "0.3"
|
||||
log = "0.3"
|
||||
reqwest = "0.2"
|
||||
reqwest = "0.4"
|
||||
mime = "0.2"
|
||||
clippy = { version = "0.0.90", optional = true}
|
||||
|
||||
|
@ -16,13 +16,14 @@
|
||||
|
||||
//! Fetching
|
||||
|
||||
use std::{io, fmt};
|
||||
use std::{io, fmt, time};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{self, AtomicBool};
|
||||
|
||||
use futures::{self, BoxFuture, Future};
|
||||
use futures_cpupool::{CpuPool, CpuFuture};
|
||||
use mime::{self, Mime};
|
||||
use parking_lot::RwLock;
|
||||
use reqwest;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
@ -73,24 +74,52 @@ pub trait Fetch: Clone + Send + Sync + 'static {
|
||||
fn close(self) where Self: Sized {}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
const CLIENT_TIMEOUT_SECONDS: u64 = 5;
|
||||
|
||||
pub struct Client {
|
||||
client: Arc<reqwest::Client>,
|
||||
client: RwLock<(time::Instant, Arc<reqwest::Client>)>,
|
||||
pool: CpuPool,
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
impl Clone for Client {
|
||||
fn clone(&self) -> Self {
|
||||
let (ref time, ref client) = *self.client.read();
|
||||
Client {
|
||||
client: RwLock::new((time.clone(), client.clone())),
|
||||
pool: self.pool.clone(),
|
||||
limit: self.limit.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
fn with_limit(limit: Option<usize>) -> Result<Self, Error> {
|
||||
fn new_client() -> Result<Arc<reqwest::Client>, Error> {
|
||||
let mut client = reqwest::Client::new()?;
|
||||
client.redirect(reqwest::RedirectPolicy::limited(5));
|
||||
Ok(Arc::new(client))
|
||||
}
|
||||
|
||||
fn with_limit(limit: Option<usize>) -> Result<Self, Error> {
|
||||
Ok(Client {
|
||||
client: Arc::new(client),
|
||||
client: RwLock::new((time::Instant::now(), Self::new_client()?)),
|
||||
pool: CpuPool::new(4),
|
||||
limit: limit,
|
||||
})
|
||||
}
|
||||
|
||||
fn client(&self) -> Result<Arc<reqwest::Client>, Error> {
|
||||
{
|
||||
let (ref time, ref client) = *self.client.read();
|
||||
if time.elapsed() < time::Duration::from_secs(CLIENT_TIMEOUT_SECONDS) {
|
||||
return Ok(client.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let client = Self::new_client()?;
|
||||
*self.client.write() = (time::Instant::now(), client.clone());
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
||||
impl Fetch for Client {
|
||||
@ -112,12 +141,19 @@ impl Fetch for Client {
|
||||
fn fetch_with_abort(&self, url: &str, abort: Abort) -> Self::Result {
|
||||
debug!(target: "fetch", "Fetching from: {:?}", url);
|
||||
|
||||
match self.client() {
|
||||
Ok(client) => {
|
||||
self.pool.spawn(FetchTask {
|
||||
url: url.into(),
|
||||
client: self.client.clone(),
|
||||
client: client,
|
||||
limit: self.limit,
|
||||
abort: abort,
|
||||
})
|
||||
},
|
||||
Err(err) => {
|
||||
self.pool.spawn(futures::future::err(err))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ extern crate log;
|
||||
|
||||
extern crate futures;
|
||||
extern crate futures_cpupool;
|
||||
extern crate parking_lot;
|
||||
extern crate reqwest;
|
||||
|
||||
pub extern crate mime;
|
||||
|
@ -77,7 +77,9 @@ pub fn ordered_trie_root<I>(input: I) -> H256
|
||||
/// assert_eq!(trie_root(v), H256::from_str(root).unwrap());
|
||||
/// }
|
||||
/// ```
|
||||
pub fn trie_root(input: Vec<(Vec<u8>, Vec<u8>)>) -> H256 {
|
||||
pub fn trie_root<I>(input: I) -> H256
|
||||
where I: IntoIterator<Item=(Vec<u8>, Vec<u8>)>
|
||||
{
|
||||
let gen_input = input
|
||||
// first put elements into btree to sort them and to remove duplicates
|
||||
.into_iter()
|
||||
|
Loading…
Reference in New Issue
Block a user